├── .npmrc ├── .python-version ├── py_src ├── __init__.py ├── server.py ├── obs-mcp.py ├── main.py ├── general.py ├── scenes.py ├── transitions.py ├── sources.py ├── client.py ├── streaming.py └── scene_items.py ├── .gitignore ├── tsconfig.json ├── src ├── index.test.ts ├── index.ts ├── tools │ ├── index.ts │ ├── streaming.ts │ ├── media-inputs.ts │ ├── sources.ts │ ├── scenes.ts │ ├── record.ts │ ├── transitions.ts │ ├── scene-items.ts │ ├── ui.ts │ ├── general.ts │ └── filters.ts └── server.ts ├── pyproject.toml ├── package.json ├── scripts └── split_docs.py ├── CLAUDE.md ├── README.md └── docs └── protocol_split ├── stream.json ├── record.json ├── media_inputs.json ├── sources.json ├── ui.json ├── transitions.json ├── outputs.json ├── scenes.json └── general.json /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /py_src/__init__.py: -------------------------------------------------------------------------------- 1 | # OBS MCP module 2 | from .server import mcp, obs_client -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | 4 | # Build output 5 | build/ 6 | dist/ 7 | 8 | # Environment variables 9 | .env 10 | 11 | # Log files 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Editor directories and files 18 | .idea/ 19 | .vscode/ 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # OS specific 27 | .DS_Store 28 | Thumbs.db 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "**/*.test.ts", "**/*.test.js"] 15 | } -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | const cliPath = path.resolve(__dirname, '../build/index.js'); 6 | 7 | describe('CLI entry (build/index.js)', () => { 8 | it('should exist after build', () => { 9 | expect(fs.existsSync(cliPath)).toBe(true); 10 | }); 11 | 12 | it('should be executable', () => { 13 | const stat = fs.statSync(cliPath); 14 | // Check owner execute bit 15 | expect(stat.mode & 0o100).toBeTruthy(); 16 | }); 17 | }); -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "obs-mcp" 3 | version = "0.1.0" 4 | description = "OBS Studio MCP Server - Control OBS via MCP tools" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [ 8 | "httpx>=0.28.1", 9 | "mcp[cli]>=1.5.0", 10 | "websockets>=12.0", 11 | ] 12 | 13 | [project.optional-dependencies] 14 | dev = [ 15 | "black>=24.3.0", 16 | "ruff>=0.3.2", 17 | ] 18 | 19 | [build-system] 20 | requires = ["hatchling"] 21 | build-backend = "hatchling.build" 22 | 23 | [tool.black] 24 | line-length = 100 25 | 26 | [tool.ruff] 27 | line-length = 100 28 | target-version = "py310" 29 | 30 | [tool.ruff.lint] 31 | select = ["E", "F", "B", "I"] -------------------------------------------------------------------------------- /py_src/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import asyncio 4 | import logging 5 | from mcp.server.fastmcp import FastMCP 6 | from .client import OBSWebSocketClient 7 | 8 | # Setup logging 9 | logger = logging.getLogger("obs_server") 10 | 11 | # Create a new event loop for the MCP server and client 12 | loop = asyncio.new_event_loop() 13 | asyncio.set_event_loop(loop) 14 | 15 | # Create a single FastMCP instance for the entire application 16 | # The FastMCP will use the default event loop we just set 17 | mcp = FastMCP("obs_mcp", description="OBS Studio MCP Server") 18 | 19 | # Create a client with the same event loop 20 | obs_client = OBSWebSocketClient(loop=loop) 21 | 22 | # Log that the server was created 23 | logger.debug("OBS MCP server created with dedicated event loop") -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obs-mcp", 3 | "version": "1.0.1", 4 | "description": "MCP server for OBS Studio", 5 | "type": "module", 6 | "bin": { 7 | "obs-mcp": "./build/index.js" 8 | }, 9 | "main": "build/index.js", 10 | "scripts": { 11 | "build": "tsc && chmod 755 build/index.js", 12 | "start": "node build/index.js", 13 | "test": "vitest run" 14 | }, 15 | "files": [ 16 | "build" 17 | ], 18 | "dependencies": { 19 | "@modelcontextprotocol/sdk": "^1.0.0", 20 | "crypto-js": "^4.2.0", 21 | "ws": "^8.16.0", 22 | "zod": "^3.22.4" 23 | }, 24 | "devDependencies": { 25 | "@types/crypto-js": "^4.2.2", 26 | "@types/node": "^20.11.25", 27 | "@types/ws": "^8.5.10", 28 | "typescript": "^5.4.2", 29 | "vitest": "1.6.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { startServer } from "./server.js"; 4 | 5 | const logger = { 6 | log: (message: string) => console.log(message), 7 | error: (message: string) => console.error(message), 8 | debug: (message: string) => console.debug(message), 9 | }; 10 | 11 | // Set up better error handling 12 | process.on("uncaughtException", (error) => { 13 | logger.error(`Uncaught exception: ${error instanceof Error ? error.message : String(error)}`); 14 | process.exit(1); 15 | }); 16 | 17 | process.on("unhandledRejection", (reason, promise) => { 18 | logger.error(`Unhandled rejection at: ${promise}, reason: ${reason}`); 19 | process.exit(1); 20 | }); 21 | 22 | // Start the server 23 | startServer().catch((error) => { 24 | logger.error(`Fatal error in main(): ${error instanceof Error ? error.message : String(error)}`); 25 | process.exit(1); 26 | }); -------------------------------------------------------------------------------- /scripts/split_docs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import json 3 | import os 4 | 5 | # Create output directory if it doesn't exist 6 | output_dir = "docs/protocol_split" 7 | os.makedirs(output_dir, exist_ok=True) 8 | 9 | # Load the protocol.json file 10 | with open("docs/protocol.json", "r") as f: 11 | protocol = json.load(f) 12 | 13 | # Get all unique categories 14 | requests = protocol["requests"] 15 | categories = set(req["category"] for req in requests) 16 | 17 | # Create a dictionary for each category 18 | category_files = { 19 | "enums": {"enums": protocol["enums"]}, 20 | "events": {"events": protocol["events"]} 21 | } 22 | 23 | # Initialize each category file with an empty requests array 24 | for category in categories: 25 | category_files[category] = {"requests": []} 26 | 27 | # Sort requests into appropriate category files 28 | for request in requests: 29 | category = request["category"] 30 | category_files[category]["requests"].append(request) 31 | 32 | # Write each category to its own file 33 | for category, data in category_files.items(): 34 | filename = f"{output_dir}/{category.replace(' ', '_')}.json" 35 | with open(filename, "w") as f: 36 | json.dump(data, f, indent=2) 37 | print(f"Created {filename}") 38 | 39 | print("Protocol split complete!") -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # OBS-MCP Development Guidelines 2 | 3 | ## Build and Development Commands 4 | - Build: `npm run build` - Compiles TypeScript and sets executable permissions 5 | - Start: `npm run start` - Runs the MCP server for OBS Studio 6 | - Python linting: `black py_src/` and `ruff check py_src/` 7 | 8 | ## Code Style Guidelines 9 | ### TypeScript 10 | - **Imports**: ES modules with `.js` extensions in import paths 11 | - **Formatting**: 2-space indentation, semicolons 12 | - **Types**: Use strict TypeScript typing with interfaces, enums, and type annotations 13 | - **Error Handling**: Wrap with try/catch blocks, include original error messages using pattern: 14 | `error instanceof Error ? error.message : String(error)` 15 | - **Logging**: Use file logger instead of console.log 16 | 17 | ### Python 18 | - **Formatting**: 4-space indentation, 100 character line length (configured in Black) 19 | - **Type Hints**: Use Python typing module 20 | - **Linting**: Black for formatting, Ruff for linting with E, F, B, I rule sets 21 | - **Docstrings**: Include docstrings for all functions 22 | - **Asyncio**: Use async/await for asynchronous operations 23 | 24 | ## Naming Conventions 25 | - **Variables/Functions**: camelCase for TypeScript, snake_case for Python 26 | - **Classes/Interfaces**: PascalCase 27 | - **Constants**: UPPER_CASE or camelCase -------------------------------------------------------------------------------- /py_src/obs-mcp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import asyncio 4 | import os 5 | import logging 6 | import sys 7 | 8 | # Configure logging 9 | logging.basicConfig( 10 | level=logging.DEBUG, 11 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 12 | handlers=[ 13 | logging.StreamHandler(sys.stdout) 14 | ] 15 | ) 16 | logger = logging.getLogger("obs_mcp") 17 | 18 | # Import the single MCP instance and client 19 | from obs_mcp import mcp, obs_client 20 | 21 | # Now import all tool modules 22 | from obs_mcp import general 23 | from obs_mcp import scenes 24 | from obs_mcp import sources 25 | from obs_mcp import scene_items 26 | from obs_mcp import streaming 27 | from obs_mcp import transitions 28 | 29 | async def startup(): 30 | """Connect to OBS WebSocket server on startup""" 31 | try: 32 | await obs_client.connect() 33 | logger.info("Connected to OBS WebSocket server") 34 | except Exception as e: 35 | logger.error(f"Failed to connect to OBS WebSocket server: {e}") 36 | logger.error("Make sure OBS is running and WebSocket server is enabled") 37 | logger.error("Will try to connect when the first request is made") 38 | 39 | async def shutdown(): 40 | """Close connection to OBS WebSocket server on shutdown""" 41 | await obs_client.close() 42 | logger.info("Disconnected from OBS WebSocket server") 43 | 44 | if __name__ == "__main__": 45 | # Register startup and shutdown functions 46 | startup_task = asyncio.ensure_future(startup()) 47 | asyncio.get_event_loop().run_until_complete(startup_task) 48 | mcp.run(transport='stdio') -------------------------------------------------------------------------------- /src/tools/index.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { OBSWebSocketClient } from "../client.js"; 3 | import { z } from "zod"; 4 | 5 | // Import specific tool modules 6 | import * as general from "./general.js"; 7 | import * as scenes from "./scenes.js"; 8 | import * as sources from "./sources.js"; 9 | import * as sceneItems from "./scene-items.js"; 10 | import * as streaming from "./streaming.js"; 11 | import * as transitions from "./transitions.js"; 12 | import * as config from "./config.js"; 13 | import * as filters from "./filters.js"; 14 | import * as inputs from "./inputs.js"; 15 | import * as mediaInputs from "./media-inputs.js"; 16 | import * as outputs from "./outputs.js"; 17 | import * as record from "./record.js"; 18 | import * as ui from "./ui.js"; 19 | 20 | // Export the initialization function for all tools 21 | export async function initialize(server: McpServer, client: OBSWebSocketClient): Promise { 22 | // Initialize all tool modules 23 | await Promise.all([ 24 | general.initialize(server, client), 25 | scenes.initialize(server, client), 26 | sources.initialize(server, client), 27 | sceneItems.initialize(server, client), 28 | streaming.initialize(server, client), 29 | transitions.initialize(server, client), 30 | config.initialize(server, client), 31 | filters.initialize(server, client), 32 | inputs.initialize(server, client), 33 | mediaInputs.initialize(server, client), 34 | outputs.initialize(server, client), 35 | record.initialize(server, client), 36 | ui.initialize(server, client) 37 | ]); 38 | } 39 | 40 | // Export tool modules 41 | export { 42 | general, 43 | scenes, 44 | sources, 45 | sceneItems, 46 | streaming, 47 | transitions, 48 | config, 49 | filters, 50 | inputs, 51 | mediaInputs, 52 | outputs, 53 | record, 54 | ui 55 | }; -------------------------------------------------------------------------------- /py_src/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os 5 | import logging 6 | import asyncio 7 | 8 | # Set up logging 9 | logging.basicConfig( 10 | level=logging.DEBUG, 11 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 12 | handlers=[ 13 | logging.StreamHandler(sys.stdout) 14 | ] 15 | ) 16 | logger = logging.getLogger("main") 17 | 18 | # Check environment 19 | if not os.environ.get("OBS_WS_PASSWORD"): 20 | logger.warning("OBS_WS_PASSWORD environment variable is not set.") 21 | logger.warning("You will need to set this to the WebSocket password configured in OBS.") 22 | logger.warning("Example: export OBS_WS_PASSWORD='your_password_here'") 23 | 24 | # Run the server 25 | if __name__ == "__main__": 26 | logger.info("Starting OBS MCP Server") 27 | 28 | try: 29 | # Import the shared event loop and server objects 30 | from obs_mcp.server import loop, mcp, obs_client 31 | 32 | # Connect to OBS WebSocket server before starting 33 | async def connect_to_obs(): 34 | try: 35 | await obs_client.connect() 36 | logger.info("Connected to OBS WebSocket server") 37 | except Exception as e: 38 | logger.error(f"Failed to connect to OBS WebSocket server: {e}") 39 | logger.error("Make sure OBS is running and WebSocket server is enabled") 40 | logger.error("Will try to connect on first request") 41 | 42 | # Connect to OBS first 43 | connect_task = loop.create_task(connect_to_obs()) 44 | loop.run_until_complete(connect_task) 45 | 46 | # Start the server 47 | logger.info("Starting MCP server...") 48 | mcp.run(transport='stdio') 49 | 50 | except Exception as e: 51 | logger.error(f"Error starting OBS MCP server: {e}") 52 | sys.exit(1) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OBS MCP Server 2 | 3 | An MCP server for OBS Studio that provides tools to control OBS via the OBS WebSocket protocol. 4 | 5 | ## Features 6 | 7 | - Connect to OBS WebSocket server 8 | - Control OBS via MCP tools 9 | - Provides tools for: 10 | - General operations 11 | - Scene management 12 | - Source control 13 | - Scene item manipulation 14 | - Streaming and recording 15 | - Transitions 16 | 17 | 18 | ## Usage 19 | 20 | 1. Make sure OBS Studio is running with WebSocket server enabled (Tools > WebSocket Server Settings). Note the password for the WS. 21 | 2. Set the WebSocket password in environment variable (if needed): 22 | 23 | ```bash 24 | export OBS_WEBSOCKET_PASSWORD="your_password_here" 25 | ``` 26 | 27 | 3. Add the MCP server to Claude desktop with the MCP server settings: 28 | 29 | ```json 30 | { 31 | "mcpServers": { 32 | "obs": { 33 | "command": "npx", 34 | "args": ["-y", "obs-mcp@latest"], 35 | "env": { 36 | "OBS_WEBSOCKET_PASSWORD": "" 37 | } 38 | } 39 | } 40 | } 41 | ``` 42 | 43 | 4. Use Claude to control your OBS! 44 | 45 | ## Development 46 | 47 | If you want to run the server locally using the code in this git repo, you can do the following: 48 | 49 | 50 | ```bash 51 | npm run build 52 | npm run start 53 | ``` 54 | 55 | Then configure Claude desktop: 56 | 57 | ```json 58 | { 59 | "mcpServers": { 60 | "obs": { 61 | "command": "node", 62 | "args": [ 63 | "/build/index.js" 64 | ], 65 | "env": { 66 | "OBS_WEBSOCKET_PASSWORD": "" 67 | } 68 | } 69 | } 70 | } 71 | ``` 72 | 73 | ## Available Tools 74 | 75 | The server provides tools organized by category: 76 | 77 | - General tools: Version info, stats, hotkeys, studio mode 78 | - Scene tools: List scenes, switch scenes, create/remove scenes 79 | - Source tools: Manage sources, settings, audio levels, mute/unmute 80 | - Scene item tools: Manage items in scenes (position, visibility, etc.) 81 | - Streaming tools: Start/stop streaming, recording, virtual camera 82 | - Transition tools: Set transitions, durations, trigger transitions 83 | 84 | ## Environment Variables 85 | 86 | - `OBS_WEBSOCKET_URL`: WebSocket URL (default: ws://localhost:4455) 87 | - `OBS_WEBSOCKET_PASSWORD`: Password for authenticating with OBS WebSocket (if required) 88 | 89 | ## Requirements 90 | 91 | - Node.js 16+ 92 | - OBS Studio 31+ with WebSocket server enabled 93 | - Claude desktop 94 | 95 | ## License 96 | 97 | See the [LICENSE](LICENSE) file for details. -------------------------------------------------------------------------------- /docs/protocol_split/stream.json: -------------------------------------------------------------------------------- 1 | { 2 | "requests": [ 3 | { 4 | "description": "Gets the status of the stream output.", 5 | "requestType": "GetStreamStatus", 6 | "complexity": 2, 7 | "rpcVersion": "1", 8 | "deprecated": false, 9 | "initialVersion": "5.0.0", 10 | "category": "stream", 11 | "requestFields": [], 12 | "responseFields": [ 13 | { 14 | "valueName": "outputActive", 15 | "valueType": "Boolean", 16 | "valueDescription": "Whether the output is active" 17 | }, 18 | { 19 | "valueName": "outputReconnecting", 20 | "valueType": "Boolean", 21 | "valueDescription": "Whether the output is currently reconnecting" 22 | }, 23 | { 24 | "valueName": "outputTimecode", 25 | "valueType": "String", 26 | "valueDescription": "Current formatted timecode string for the output" 27 | }, 28 | { 29 | "valueName": "outputDuration", 30 | "valueType": "Number", 31 | "valueDescription": "Current duration in milliseconds for the output" 32 | }, 33 | { 34 | "valueName": "outputCongestion", 35 | "valueType": "Number", 36 | "valueDescription": "Congestion of the output" 37 | }, 38 | { 39 | "valueName": "outputBytes", 40 | "valueType": "Number", 41 | "valueDescription": "Number of bytes sent by the output" 42 | }, 43 | { 44 | "valueName": "outputSkippedFrames", 45 | "valueType": "Number", 46 | "valueDescription": "Number of frames skipped by the output's process" 47 | }, 48 | { 49 | "valueName": "outputTotalFrames", 50 | "valueType": "Number", 51 | "valueDescription": "Total number of frames delivered by the output's process" 52 | } 53 | ] 54 | }, 55 | { 56 | "description": "Toggles the status of the stream output.", 57 | "requestType": "ToggleStream", 58 | "complexity": 1, 59 | "rpcVersion": "1", 60 | "deprecated": false, 61 | "initialVersion": "5.0.0", 62 | "category": "stream", 63 | "requestFields": [], 64 | "responseFields": [ 65 | { 66 | "valueName": "outputActive", 67 | "valueType": "Boolean", 68 | "valueDescription": "New state of the stream output" 69 | } 70 | ] 71 | }, 72 | { 73 | "description": "Starts the stream output.", 74 | "requestType": "StartStream", 75 | "complexity": 1, 76 | "rpcVersion": "1", 77 | "deprecated": false, 78 | "initialVersion": "5.0.0", 79 | "category": "stream", 80 | "requestFields": [], 81 | "responseFields": [] 82 | }, 83 | { 84 | "description": "Stops the stream output.", 85 | "requestType": "StopStream", 86 | "complexity": 1, 87 | "rpcVersion": "1", 88 | "deprecated": false, 89 | "initialVersion": "5.0.0", 90 | "category": "stream", 91 | "requestFields": [], 92 | "responseFields": [] 93 | }, 94 | { 95 | "description": "Sends CEA-608 caption text over the stream output.", 96 | "requestType": "SendStreamCaption", 97 | "complexity": 2, 98 | "rpcVersion": "1", 99 | "deprecated": false, 100 | "initialVersion": "5.0.0", 101 | "category": "stream", 102 | "requestFields": [ 103 | { 104 | "valueName": "captionText", 105 | "valueType": "String", 106 | "valueDescription": "Caption text", 107 | "valueRestrictions": null, 108 | "valueOptional": false, 109 | "valueOptionalBehavior": null 110 | } 111 | ], 112 | "responseFields": [] 113 | } 114 | ] 115 | } -------------------------------------------------------------------------------- /py_src/general.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from typing import Any, Dict, List, Optional 4 | 5 | from .client import obs_client 6 | from .server import mcp 7 | 8 | @mcp.tool() 9 | async def get_version() -> Dict[str, Any]: 10 | """ 11 | Gets data about the current plugin and RPC version. 12 | 13 | Returns: 14 | Dict containing OBS version information including obsVersion, 15 | obsWebSocketVersion, rpcVersion, and available requests list 16 | """ 17 | return await obs_client.send_request("GetVersion") 18 | 19 | @mcp.tool() 20 | async def get_stats() -> Dict[str, Any]: 21 | """ 22 | Gets statistics about OBS, obs-websocket, and the current session. 23 | 24 | Returns: 25 | Dict containing various OBS statistics including CPU usage, memory usage, 26 | available disk space, and session information 27 | """ 28 | return await obs_client.send_request("GetStats") 29 | 30 | @mcp.tool() 31 | async def broadcast_custom_event(event_data: Dict[str, Any]) -> None: 32 | """ 33 | Broadcasts a custom event to all WebSocket clients. 34 | 35 | Args: 36 | event_data: Data to send with the event 37 | """ 38 | await obs_client.send_request("BroadcastCustomEvent", {"eventData": event_data}) 39 | 40 | @mcp.tool() 41 | async def call_vendor_request(vendor_name: str, request_type: str, request_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: 42 | """ 43 | Call a request registered to a vendor. 44 | 45 | Args: 46 | vendor_name: Name of the vendor to use 47 | request_type: The request type to call 48 | request_data: Additional data to pass to the request 49 | 50 | Returns: 51 | Response data from the vendor request 52 | """ 53 | payload = { 54 | "vendorName": vendor_name, 55 | "requestType": request_type 56 | } 57 | if request_data: 58 | payload["requestData"] = request_data 59 | 60 | return await obs_client.send_request("CallVendorRequest", payload) 61 | 62 | @mcp.tool() 63 | async def get_hot_key_list() -> List[str]: 64 | """ 65 | Gets an array of all available hotkey names. 66 | 67 | Returns: 68 | List of hotkey names 69 | """ 70 | response = await obs_client.send_request("GetHotkeyList") 71 | return response.get("hotkeys", []) 72 | 73 | @mcp.tool() 74 | async def trigger_hotkey_by_name(hotkey_name: str) -> None: 75 | """ 76 | Triggers a hotkey using its name. 77 | 78 | Args: 79 | hotkey_name: Name of the hotkey to trigger 80 | """ 81 | await obs_client.send_request("TriggerHotkeyByName", {"hotkeyName": hotkey_name}) 82 | 83 | @mcp.tool() 84 | async def trigger_hotkey_by_key_sequence(key_id: str, press_shift: bool = False, 85 | press_ctrl: bool = False, press_alt: bool = False, 86 | press_cmd: bool = False) -> None: 87 | """ 88 | Triggers a hotkey using a sequence of keys. 89 | 90 | Args: 91 | key_id: The key to use (e.g., "OBS_KEY_A") 92 | press_shift: Whether to press the Shift key 93 | press_ctrl: Whether to press the Control key 94 | press_alt: Whether to press the Alt key 95 | press_cmd: Whether to press the Command key (macOS) 96 | """ 97 | payload = { 98 | "keyId": key_id, 99 | "keyModifiers": { 100 | "shift": press_shift, 101 | "control": press_ctrl, 102 | "alt": press_alt, 103 | "cmd": press_cmd 104 | } 105 | } 106 | 107 | await obs_client.send_request("TriggerHotkeyByKeySequence", payload) 108 | 109 | @mcp.tool() 110 | async def sleep(sleep_milliseconds: int) -> None: 111 | """ 112 | Sleeps for a specified amount of time (in milliseconds). 113 | 114 | Args: 115 | sleep_milliseconds: Number of milliseconds to sleep for 116 | """ 117 | await obs_client.send_request("Sleep", {"sleepMillis": sleep_milliseconds}) -------------------------------------------------------------------------------- /src/tools/streaming.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { OBSWebSocketClient } from "../client.js"; 3 | import { z } from "zod"; 4 | 5 | export async function initialize(server: McpServer, client: OBSWebSocketClient): Promise { 6 | // GetStreamStatus tool 7 | server.tool( 8 | "obs-get-stream-status", 9 | "Get the current streaming status", 10 | {}, 11 | async () => { 12 | try { 13 | const status = await client.sendRequest("GetStreamStatus"); 14 | return { 15 | content: [ 16 | { 17 | type: "text", 18 | text: JSON.stringify(status, null, 2) 19 | } 20 | ] 21 | }; 22 | } catch (error) { 23 | return { 24 | content: [ 25 | { 26 | type: "text", 27 | text: `Error getting stream status: ${error instanceof Error ? error.message : String(error)}` 28 | } 29 | ], 30 | isError: true 31 | }; 32 | } 33 | } 34 | ); 35 | 36 | // StartStream tool 37 | server.tool( 38 | "obs-start-stream", 39 | "Start streaming in OBS", 40 | {}, 41 | async () => { 42 | try { 43 | await client.sendRequest("StartStream"); 44 | return { 45 | content: [ 46 | { 47 | type: "text", 48 | text: "Successfully started streaming" 49 | } 50 | ] 51 | }; 52 | } catch (error) { 53 | return { 54 | content: [ 55 | { 56 | type: "text", 57 | text: `Error starting stream: ${error instanceof Error ? error.message : String(error)}` 58 | } 59 | ], 60 | isError: true 61 | }; 62 | } 63 | } 64 | ); 65 | 66 | // StopStream tool 67 | server.tool( 68 | "obs-stop-stream", 69 | "Stop streaming in OBS", 70 | {}, 71 | async () => { 72 | try { 73 | await client.sendRequest("StopStream"); 74 | return { 75 | content: [ 76 | { 77 | type: "text", 78 | text: "Successfully stopped streaming" 79 | } 80 | ] 81 | }; 82 | } catch (error) { 83 | return { 84 | content: [ 85 | { 86 | type: "text", 87 | text: `Error stopping stream: ${error instanceof Error ? error.message : String(error)}` 88 | } 89 | ], 90 | isError: true 91 | }; 92 | } 93 | } 94 | ); 95 | 96 | // ToggleStream tool 97 | server.tool( 98 | "obs-toggle-stream", 99 | "Toggle the streaming state in OBS", 100 | {}, 101 | async () => { 102 | try { 103 | const response = await client.sendRequest("ToggleStream"); 104 | return { 105 | content: [ 106 | { 107 | type: "text", 108 | text: `Successfully toggled streaming state. Stream is now ${response.outputActive ? 'active' : 'inactive'}` 109 | } 110 | ] 111 | }; 112 | } catch (error) { 113 | return { 114 | content: [ 115 | { 116 | type: "text", 117 | text: `Error toggling stream: ${error instanceof Error ? error.message : String(error)}` 118 | } 119 | ], 120 | isError: true 121 | }; 122 | } 123 | } 124 | ); 125 | 126 | // SendStreamCaption tool 127 | server.tool( 128 | "obs-send-stream-caption", 129 | "Sends CEA-608 caption text over the stream output", 130 | { 131 | captionText: z.string().describe("Caption text to send") 132 | }, 133 | async ({ captionText }) => { 134 | try { 135 | await client.sendRequest("SendStreamCaption", { captionText }); 136 | return { 137 | content: [ 138 | { 139 | type: "text", 140 | text: "Successfully sent stream caption" 141 | } 142 | ] 143 | }; 144 | } catch (error) { 145 | return { 146 | content: [ 147 | { 148 | type: "text", 149 | text: `Error sending stream caption: ${error instanceof Error ? error.message : String(error)}` 150 | } 151 | ], 152 | isError: true 153 | }; 154 | } 155 | } 156 | ); 157 | } -------------------------------------------------------------------------------- /py_src/scenes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from typing import Any, Dict, List, Optional 4 | 5 | from .client import obs_client 6 | from .server import mcp 7 | 8 | @mcp.tool() 9 | async def get_scene_list() -> Dict[str, Any]: 10 | """ 11 | Gets an array of all scenes in OBS. 12 | 13 | Returns: 14 | Dict containing: 15 | - currentProgramSceneName: Name of the current program scene 16 | - currentPreviewSceneName: Name of the current preview scene (if studio mode is enabled) 17 | - scenes: Array of scenes (each with name, sceneIndex) 18 | """ 19 | return await obs_client.send_request("GetSceneList") 20 | 21 | @mcp.tool() 22 | async def get_group_list() -> List[str]: 23 | """ 24 | Gets an array of all groups in OBS. 25 | 26 | Returns: 27 | List of group names 28 | """ 29 | response = await obs_client.send_request("GetGroupList") 30 | return response.get("groups", []) 31 | 32 | @mcp.tool() 33 | async def get_current_program_scene() -> str: 34 | """ 35 | Gets the current program scene. 36 | 37 | Returns: 38 | Name of the current program scene 39 | """ 40 | response = await obs_client.send_request("GetCurrentProgramScene") 41 | return response.get("currentProgramSceneName", "") 42 | 43 | @mcp.tool() 44 | async def set_current_program_scene(scene_name: str) -> None: 45 | """ 46 | Sets the current program scene. 47 | 48 | Args: 49 | scene_name: Name of the scene to set as program 50 | """ 51 | await obs_client.send_request("SetCurrentProgramScene", {"sceneName": scene_name}) 52 | 53 | @mcp.tool() 54 | async def get_current_preview_scene() -> str: 55 | """ 56 | Gets the current preview scene (only available when studio mode is enabled). 57 | 58 | Returns: 59 | Name of the current preview scene 60 | """ 61 | response = await obs_client.send_request("GetCurrentPreviewScene") 62 | return response.get("currentPreviewSceneName", "") 63 | 64 | @mcp.tool() 65 | async def set_current_preview_scene(scene_name: str) -> None: 66 | """ 67 | Sets the current preview scene (only available when studio mode is enabled). 68 | 69 | Args: 70 | scene_name: Name of the scene to set as preview 71 | """ 72 | await obs_client.send_request("SetCurrentPreviewScene", {"sceneName": scene_name}) 73 | 74 | @mcp.tool() 75 | async def create_scene(scene_name: str) -> None: 76 | """ 77 | Creates a new scene in OBS. 78 | 79 | Args: 80 | scene_name: Name of the scene to create 81 | """ 82 | await obs_client.send_request("CreateScene", {"sceneName": scene_name}) 83 | 84 | @mcp.tool() 85 | async def remove_scene(scene_name: str) -> None: 86 | """ 87 | Removes a scene from OBS. 88 | 89 | Args: 90 | scene_name: Name of the scene to remove 91 | """ 92 | await obs_client.send_request("RemoveScene", {"sceneName": scene_name}) 93 | 94 | @mcp.tool() 95 | async def set_scene_name(scene_name: str, new_scene_name: str) -> None: 96 | """ 97 | Sets the name of a scene (rename). 98 | 99 | Args: 100 | scene_name: Current name of the scene 101 | new_scene_name: New name for the scene 102 | """ 103 | await obs_client.send_request("SetSceneName", { 104 | "sceneName": scene_name, 105 | "newSceneName": new_scene_name 106 | }) 107 | 108 | @mcp.tool() 109 | async def get_scene_scene_transition_override(scene_name: str) -> Dict[str, Any]: 110 | """ 111 | Gets the scene transition override for a scene. 112 | 113 | Args: 114 | scene_name: Name of the scene 115 | 116 | Returns: 117 | Dict containing transition name and duration (if override exists) 118 | """ 119 | return await obs_client.send_request("GetSceneSceneTransitionOverride", {"sceneName": scene_name}) 120 | 121 | @mcp.tool() 122 | async def set_scene_scene_transition_override(scene_name: str, transition_name: Optional[str] = None, 123 | transition_duration: Optional[int] = None) -> None: 124 | """ 125 | Sets the scene transition override for a scene. 126 | 127 | Args: 128 | scene_name: Name of the scene 129 | transition_name: Name of the transition to use, or null to remove 130 | transition_duration: Duration in milliseconds of the transition, or null to use default 131 | """ 132 | payload = {"sceneName": scene_name} 133 | 134 | if transition_name is not None: 135 | payload["transitionName"] = transition_name 136 | 137 | if transition_duration is not None: 138 | payload["transitionDuration"] = transition_duration 139 | 140 | await obs_client.send_request("SetSceneSceneTransitionOverride", payload) -------------------------------------------------------------------------------- /py_src/transitions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from typing import Any, Dict, List, Optional 4 | 5 | from .client import obs_client 6 | from .server import mcp 7 | 8 | @mcp.tool() 9 | async def get_transition_kind_list() -> List[str]: 10 | """ 11 | Gets an array of all available transition kinds. 12 | 13 | Returns: 14 | List of transition kinds 15 | """ 16 | response = await obs_client.send_request("GetTransitionKindList") 17 | return response.get("transitionKinds", []) 18 | 19 | @mcp.tool() 20 | async def get_scene_transition_list() -> Dict[str, Any]: 21 | """ 22 | Gets an array of all scene transitions in OBS. 23 | 24 | Returns: 25 | Dict containing: 26 | - currentSceneTransitionKind: Kind of the current scene transition 27 | - currentSceneTransitionName: Name of the current scene transition 28 | - currentSceneTransitionDuration: Duration of the current scene transition (in milliseconds) 29 | - transitions: Array of transitions (each with name, kind) 30 | """ 31 | return await obs_client.send_request("GetSceneTransitionList") 32 | 33 | @mcp.tool() 34 | async def get_current_scene_transition() -> Dict[str, Any]: 35 | """ 36 | Gets information about the current scene transition. 37 | 38 | Returns: 39 | Dict containing transition information including kind, name, duration, and settings 40 | """ 41 | return await obs_client.send_request("GetCurrentSceneTransition") 42 | 43 | @mcp.tool() 44 | async def set_current_scene_transition(transition_name: str) -> None: 45 | """ 46 | Sets the current scene transition. 47 | 48 | Args: 49 | transition_name: Name of the transition to set as current 50 | """ 51 | await obs_client.send_request("SetCurrentSceneTransition", {"transitionName": transition_name}) 52 | 53 | @mcp.tool() 54 | async def set_current_scene_transition_duration(transition_duration: int) -> None: 55 | """ 56 | Sets the duration of the current scene transition (if supported). 57 | 58 | Args: 59 | transition_duration: Duration in milliseconds 60 | """ 61 | await obs_client.send_request("SetCurrentSceneTransitionDuration", 62 | {"transitionDuration": transition_duration}) 63 | 64 | @mcp.tool() 65 | async def set_current_scene_transition_settings(transition_settings: Dict[str, Any], overlay: bool = True) -> None: 66 | """ 67 | Sets the settings of the current scene transition. 68 | 69 | Args: 70 | transition_settings: Settings object to apply to the transition 71 | overlay: Whether to overlay with existing settings or replace them 72 | """ 73 | await obs_client.send_request("SetCurrentSceneTransitionSettings", { 74 | "transitionSettings": transition_settings, 75 | "overlay": overlay 76 | }) 77 | 78 | @mcp.tool() 79 | async def get_scene_transition_override(scene_name: str) -> Dict[str, Any]: 80 | """ 81 | Gets the scene transition override for a scene. 82 | 83 | Args: 84 | scene_name: Name of the scene 85 | 86 | Returns: 87 | Dict containing transition name and duration (if override exists) 88 | """ 89 | return await obs_client.send_request("GetSceneSceneTransitionOverride", {"sceneName": scene_name}) 90 | 91 | @mcp.tool() 92 | async def set_scene_transition_override(scene_name: str, transition_name: Optional[str] = None, 93 | transition_duration: Optional[int] = None) -> None: 94 | """ 95 | Sets the scene transition override for a scene. 96 | 97 | Args: 98 | scene_name: Name of the scene 99 | transition_name: Name of the transition to use, or null to remove 100 | transition_duration: Duration in milliseconds of the transition, or null to use default 101 | """ 102 | payload = {"sceneName": scene_name} 103 | 104 | if transition_name is not None: 105 | payload["transitionName"] = transition_name 106 | 107 | if transition_duration is not None: 108 | payload["transitionDuration"] = transition_duration 109 | 110 | await obs_client.send_request("SetSceneSceneTransitionOverride", payload) 111 | 112 | @mcp.tool() 113 | async def trigger_studio_mode_transition() -> None: 114 | """ 115 | Triggers the current scene transition. Only available when studio mode is enabled. 116 | """ 117 | await obs_client.send_request("TriggerStudioModeTransition") 118 | 119 | @mcp.tool() 120 | async def set_tbar_position(tbar_position: float) -> None: 121 | """ 122 | Sets the position of the transition bar. 123 | 124 | Args: 125 | tbar_position: Position to set the T-bar to (0.0-1.0) 126 | """ 127 | await obs_client.send_request("SetTBarPosition", {"position": tbar_position}) -------------------------------------------------------------------------------- /docs/protocol_split/record.json: -------------------------------------------------------------------------------- 1 | { 2 | "requests": [ 3 | { 4 | "description": "Gets the status of the record output.", 5 | "requestType": "GetRecordStatus", 6 | "complexity": 2, 7 | "rpcVersion": "1", 8 | "deprecated": false, 9 | "initialVersion": "5.0.0", 10 | "category": "record", 11 | "requestFields": [], 12 | "responseFields": [ 13 | { 14 | "valueName": "outputActive", 15 | "valueType": "Boolean", 16 | "valueDescription": "Whether the output is active" 17 | }, 18 | { 19 | "valueName": "outputPaused", 20 | "valueType": "Boolean", 21 | "valueDescription": "Whether the output is paused" 22 | }, 23 | { 24 | "valueName": "outputTimecode", 25 | "valueType": "String", 26 | "valueDescription": "Current formatted timecode string for the output" 27 | }, 28 | { 29 | "valueName": "outputDuration", 30 | "valueType": "Number", 31 | "valueDescription": "Current duration in milliseconds for the output" 32 | }, 33 | { 34 | "valueName": "outputBytes", 35 | "valueType": "Number", 36 | "valueDescription": "Number of bytes sent by the output" 37 | } 38 | ] 39 | }, 40 | { 41 | "description": "Toggles the status of the record output.", 42 | "requestType": "ToggleRecord", 43 | "complexity": 1, 44 | "rpcVersion": "1", 45 | "deprecated": false, 46 | "initialVersion": "5.0.0", 47 | "category": "record", 48 | "requestFields": [], 49 | "responseFields": [ 50 | { 51 | "valueName": "outputActive", 52 | "valueType": "Boolean", 53 | "valueDescription": "The new active state of the output" 54 | } 55 | ] 56 | }, 57 | { 58 | "description": "Starts the record output.", 59 | "requestType": "StartRecord", 60 | "complexity": 1, 61 | "rpcVersion": "1", 62 | "deprecated": false, 63 | "initialVersion": "5.0.0", 64 | "category": "record", 65 | "requestFields": [], 66 | "responseFields": [] 67 | }, 68 | { 69 | "description": "Stops the record output.", 70 | "requestType": "StopRecord", 71 | "complexity": 1, 72 | "rpcVersion": "1", 73 | "deprecated": false, 74 | "initialVersion": "5.0.0", 75 | "category": "record", 76 | "requestFields": [], 77 | "responseFields": [ 78 | { 79 | "valueName": "outputPath", 80 | "valueType": "String", 81 | "valueDescription": "File name for the saved recording" 82 | } 83 | ] 84 | }, 85 | { 86 | "description": "Toggles pause on the record output.", 87 | "requestType": "ToggleRecordPause", 88 | "complexity": 1, 89 | "rpcVersion": "1", 90 | "deprecated": false, 91 | "initialVersion": "5.0.0", 92 | "category": "record", 93 | "requestFields": [], 94 | "responseFields": [] 95 | }, 96 | { 97 | "description": "Pauses the record output.", 98 | "requestType": "PauseRecord", 99 | "complexity": 1, 100 | "rpcVersion": "1", 101 | "deprecated": false, 102 | "initialVersion": "5.0.0", 103 | "category": "record", 104 | "requestFields": [], 105 | "responseFields": [] 106 | }, 107 | { 108 | "description": "Resumes the record output.", 109 | "requestType": "ResumeRecord", 110 | "complexity": 1, 111 | "rpcVersion": "1", 112 | "deprecated": false, 113 | "initialVersion": "5.0.0", 114 | "category": "record", 115 | "requestFields": [], 116 | "responseFields": [] 117 | }, 118 | { 119 | "description": "Splits the current file being recorded into a new file.", 120 | "requestType": "SplitRecordFile", 121 | "complexity": 2, 122 | "rpcVersion": "1", 123 | "deprecated": false, 124 | "initialVersion": "5.5.0", 125 | "category": "record", 126 | "requestFields": [], 127 | "responseFields": [] 128 | }, 129 | { 130 | "description": "Adds a new chapter marker to the file currently being recorded.\n\nNote: As of OBS 30.2.0, the only file format supporting this feature is Hybrid MP4.", 131 | "requestType": "CreateRecordChapter", 132 | "complexity": 2, 133 | "rpcVersion": "1", 134 | "deprecated": false, 135 | "initialVersion": "5.5.0", 136 | "category": "record", 137 | "requestFields": [ 138 | { 139 | "valueName": "chapterName", 140 | "valueType": "String", 141 | "valueDescription": "Name of the new chapter", 142 | "valueRestrictions": null, 143 | "valueOptional": true, 144 | "valueOptionalBehavior": "Unknown" 145 | } 146 | ], 147 | "responseFields": [] 148 | } 149 | ] 150 | } -------------------------------------------------------------------------------- /src/tools/media-inputs.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { OBSWebSocketClient } from "../client.js"; 3 | import { z } from "zod"; 4 | 5 | export async function initialize(server: McpServer, client: OBSWebSocketClient): Promise { 6 | // GetMediaInputStatus tool 7 | server.tool( 8 | "obs-get-media-input-status", 9 | "Gets the status of a media input", 10 | { 11 | inputName: z.string().describe("Name of the media input") 12 | }, 13 | async ({ inputName }) => { 14 | try { 15 | const response = await client.sendRequest("GetMediaInputStatus", { inputName }); 16 | return { 17 | content: [ 18 | { 19 | type: "text", 20 | text: JSON.stringify(response, null, 2) 21 | } 22 | ] 23 | }; 24 | } catch (error) { 25 | return { 26 | content: [ 27 | { 28 | type: "text", 29 | text: `Error getting media input status: ${error instanceof Error ? error.message : String(error)}` 30 | } 31 | ], 32 | isError: true 33 | }; 34 | } 35 | } 36 | ); 37 | 38 | // SetMediaInputCursor tool 39 | server.tool( 40 | "obs-set-media-input-cursor", 41 | "Sets the cursor position of a media input", 42 | { 43 | inputName: z.string().describe("Name of the media input"), 44 | mediaCursor: z.number().min(0).describe("New cursor position to set (in milliseconds)") 45 | }, 46 | async ({ inputName, mediaCursor }) => { 47 | try { 48 | await client.sendRequest("SetMediaInputCursor", { inputName, mediaCursor }); 49 | return { 50 | content: [ 51 | { 52 | type: "text", 53 | text: `Successfully set media cursor position to ${mediaCursor}ms for input: ${inputName}` 54 | } 55 | ] 56 | }; 57 | } catch (error) { 58 | return { 59 | content: [ 60 | { 61 | type: "text", 62 | text: `Error setting media input cursor: ${error instanceof Error ? error.message : String(error)}` 63 | } 64 | ], 65 | isError: true 66 | }; 67 | } 68 | } 69 | ); 70 | 71 | // OffsetMediaInputCursor tool 72 | server.tool( 73 | "obs-offset-media-input-cursor", 74 | "Offsets the current cursor position of a media input", 75 | { 76 | inputName: z.string().describe("Name of the media input"), 77 | mediaCursorOffset: z.number().describe("Value to offset the current cursor position by (in milliseconds)") 78 | }, 79 | async ({ inputName, mediaCursorOffset }) => { 80 | try { 81 | await client.sendRequest("OffsetMediaInputCursor", { inputName, mediaCursorOffset }); 82 | return { 83 | content: [ 84 | { 85 | type: "text", 86 | text: `Successfully offset media cursor position by ${mediaCursorOffset}ms for input: ${inputName}` 87 | } 88 | ] 89 | }; 90 | } catch (error) { 91 | return { 92 | content: [ 93 | { 94 | type: "text", 95 | text: `Error offsetting media input cursor: ${error instanceof Error ? error.message : String(error)}` 96 | } 97 | ], 98 | isError: true 99 | }; 100 | } 101 | } 102 | ); 103 | 104 | // TriggerMediaInputAction tool 105 | server.tool( 106 | "obs-trigger-media-input-action", 107 | "Triggers an action on a media input", 108 | { 109 | inputName: z.string().describe("Name of the media input"), 110 | mediaAction: z.enum([ 111 | "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PLAY", 112 | "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PAUSE", 113 | "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_STOP", 114 | "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_RESTART", 115 | "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_NEXT", 116 | "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PREVIOUS" 117 | ]).describe("Action to trigger (PLAY, PAUSE, STOP, RESTART, NEXT, PREVIOUS)") 118 | }, 119 | async ({ inputName, mediaAction }) => { 120 | try { 121 | await client.sendRequest("TriggerMediaInputAction", { inputName, mediaAction }); 122 | return { 123 | content: [ 124 | { 125 | type: "text", 126 | text: `Successfully triggered media action '${mediaAction}' for input: ${inputName}` 127 | } 128 | ] 129 | }; 130 | } catch (error) { 131 | return { 132 | content: [ 133 | { 134 | type: "text", 135 | text: `Error triggering media input action: ${error instanceof Error ? error.message : String(error)}` 136 | } 137 | ], 138 | isError: true 139 | }; 140 | } 141 | } 142 | ); 143 | } -------------------------------------------------------------------------------- /src/tools/sources.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { OBSWebSocketClient } from "../client.js"; 3 | import { z } from "zod"; 4 | 5 | export async function initialize(server: McpServer, client: OBSWebSocketClient): Promise { 6 | // GetSourceActive tool 7 | server.tool( 8 | "obs-get-source-active", 9 | "Gets the active and show state of a source", 10 | { 11 | sourceName: z.string().optional().describe("Name of the source to get the active state of"), 12 | sourceUuid: z.string().optional().describe("UUID of the source to get the active state of") 13 | }, 14 | async ({ sourceName, sourceUuid }) => { 15 | try { 16 | const response = await client.sendRequest("GetSourceActive", { 17 | sourceName, 18 | sourceUuid 19 | }); 20 | 21 | return { 22 | content: [ 23 | { 24 | type: "text", 25 | text: JSON.stringify(response, null, 2) 26 | } 27 | ] 28 | }; 29 | } catch (error) { 30 | return { 31 | content: [ 32 | { 33 | type: "text", 34 | text: `Error getting source active state: ${error instanceof Error ? error.message : String(error)}` 35 | } 36 | ], 37 | isError: true 38 | }; 39 | } 40 | } 41 | ); 42 | 43 | // GetSourceScreenshot tool 44 | server.tool( 45 | "obs-get-source-screenshot", 46 | "Gets a Base64-encoded screenshot of a source", 47 | { 48 | sourceName: z.string().optional().describe("Name of the source to take a screenshot of"), 49 | sourceUuid: z.string().optional().describe("UUID of the source to take a screenshot of"), 50 | imageFormat: z.string().describe("Image compression format to use"), 51 | imageWidth: z.number().optional().describe("Width to scale the screenshot to"), 52 | imageHeight: z.number().optional().describe("Height to scale the screenshot to"), 53 | imageCompressionQuality: z.number().optional().describe("Compression quality to use (0-100, -1 for default)") 54 | }, 55 | async ({ sourceName, sourceUuid, imageFormat, imageWidth, imageHeight, imageCompressionQuality }) => { 56 | try { 57 | const response = await client.sendRequest("GetSourceScreenshot", { 58 | sourceName, 59 | sourceUuid, 60 | imageFormat, 61 | imageWidth, 62 | imageHeight, 63 | imageCompressionQuality 64 | }); 65 | 66 | return { 67 | content: [ 68 | { 69 | type: "text", 70 | text: `Screenshot data: ${response.imageData.substring(0, 100)}...` 71 | } 72 | ] 73 | }; 74 | } catch (error) { 75 | return { 76 | content: [ 77 | { 78 | type: "text", 79 | text: `Error getting source screenshot: ${error instanceof Error ? error.message : String(error)}` 80 | } 81 | ], 82 | isError: true 83 | }; 84 | } 85 | } 86 | ); 87 | 88 | // SaveSourceScreenshot tool 89 | server.tool( 90 | "obs-save-source-screenshot", 91 | "Saves a screenshot of a source to the filesystem", 92 | { 93 | sourceName: z.string().optional().describe("Name of the source to take a screenshot of"), 94 | sourceUuid: z.string().optional().describe("UUID of the source to take a screenshot of"), 95 | imageFormat: z.string().describe("Image compression format to use"), 96 | imageFilePath: z.string().describe("Path to save the screenshot file to"), 97 | imageWidth: z.number().optional().describe("Width to scale the screenshot to"), 98 | imageHeight: z.number().optional().describe("Height to scale the screenshot to"), 99 | imageCompressionQuality: z.number().optional().describe("Compression quality to use (0-100, -1 for default)") 100 | }, 101 | async ({ sourceName, sourceUuid, imageFormat, imageFilePath, imageWidth, imageHeight, imageCompressionQuality }) => { 102 | try { 103 | await client.sendRequest("SaveSourceScreenshot", { 104 | sourceName, 105 | sourceUuid, 106 | imageFormat, 107 | imageFilePath, 108 | imageWidth, 109 | imageHeight, 110 | imageCompressionQuality 111 | }); 112 | 113 | return { 114 | content: [ 115 | { 116 | type: "text", 117 | text: `Successfully saved screenshot to: ${imageFilePath}` 118 | } 119 | ] 120 | }; 121 | } catch (error) { 122 | return { 123 | content: [ 124 | { 125 | type: "text", 126 | text: `Error saving source screenshot: ${error instanceof Error ? error.message : String(error)}` 127 | } 128 | ], 129 | isError: true 130 | }; 131 | } 132 | } 133 | ); 134 | } -------------------------------------------------------------------------------- /py_src/sources.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from typing import Any, Dict, List, Optional 4 | from .client import obs_client 5 | from .server import mcp 6 | 7 | @mcp.tool() 8 | async def get_source_active(source_name: str) -> bool: 9 | """ 10 | Gets the active status of a source. 11 | 12 | Args: 13 | source_name: Name of the source to get the active status of 14 | 15 | Returns: 16 | Whether the source is active 17 | """ 18 | response = await obs_client.send_request("GetSourceActive", {"sourceName": source_name}) 19 | return response.get("sourceActive", False) 20 | 21 | @mcp.tool() 22 | async def get_source_screenshot(source_name: str, image_format: str = "png", 23 | image_width: int = 0, image_height: int = 0, 24 | image_compression_quality: int = -1) -> str: 25 | """ 26 | Gets a Base64-encoded screenshot of a source. 27 | 28 | Args: 29 | source_name: Name of the source to get a screenshot of 30 | image_format: Image format (png, jpeg, bmp, tga, gif) 31 | image_width: Screenshot width (0 = source width) 32 | image_height: Screenshot height (0 = source height) 33 | image_compression_quality: Compression quality (1-100, -1 = default) 34 | 35 | Returns: 36 | Base64-encoded screenshot image 37 | """ 38 | payload = { 39 | "sourceName": source_name, 40 | "imageFormat": image_format 41 | } 42 | 43 | if image_width > 0: 44 | payload["imageWidth"] = image_width 45 | 46 | if image_height > 0: 47 | payload["imageHeight"] = image_height 48 | 49 | if image_compression_quality >= 0: 50 | payload["imageCompressionQuality"] = image_compression_quality 51 | 52 | response = await obs_client.send_request("GetSourceScreenshot", payload) 53 | return response.get("imageData", "") 54 | 55 | @mcp.tool() 56 | async def save_source_screenshot(source_name: str, file_path: str, image_format: str = "png", 57 | image_width: int = 0, image_height: int = 0, 58 | image_compression_quality: int = -1) -> str: 59 | """ 60 | Saves a screenshot of a source to a file. 61 | 62 | Args: 63 | source_name: Name of the source to get a screenshot of 64 | file_path: Path to save the screenshot to 65 | image_format: Image format (png, jpeg, bmp, tga, gif) 66 | image_width: Screenshot width (0 = source width) 67 | image_height: Screenshot height (0 = source height) 68 | image_compression_quality: Compression quality (1-100, -1 = default) 69 | 70 | Returns: 71 | Path to the saved screenshot 72 | """ 73 | payload = { 74 | "sourceName": source_name, 75 | "imageFormat": image_format, 76 | "imageFilePath": file_path 77 | } 78 | 79 | if image_width > 0: 80 | payload["imageWidth"] = image_width 81 | 82 | if image_height > 0: 83 | payload["imageHeight"] = image_height 84 | 85 | if image_compression_quality >= 0: 86 | payload["imageCompressionQuality"] = image_compression_quality 87 | 88 | response = await obs_client.send_request("SaveSourceScreenshot", payload) 89 | return response.get("imageData", "") 90 | 91 | @mcp.tool() 92 | async def get_source_filter_list(source_name: str) -> Dict[str, Any]: 93 | """ 94 | Gets a list of filters on a source. 95 | 96 | Args: 97 | source_name: Name of the source to get the filters of 98 | 99 | Returns: 100 | Dict with filters array (each with name, kind, index, settings) 101 | """ 102 | return await obs_client.send_request("GetSourceFilterList", {"sourceName": source_name}) 103 | 104 | @mcp.tool() 105 | async def get_source_filter_default_settings(filter_kind: str) -> Dict[str, Any]: 106 | """ 107 | Gets the default settings for a filter kind. 108 | 109 | Args: 110 | filter_kind: Filter type to get the default settings for 111 | 112 | Returns: 113 | Dict with default filter settings 114 | """ 115 | return await obs_client.send_request("GetSourceFilterDefaultSettings", {"filterKind": filter_kind}) 116 | 117 | @mcp.tool() 118 | async def create_source_filter(source_name: str, filter_name: str, filter_kind: str, 119 | filter_settings: Optional[Dict[str, Any]] = None) -> None: 120 | """ 121 | Creates a new filter on a source. 122 | 123 | Args: 124 | source_name: Name of the source to add the filter to 125 | filter_name: Name for the new filter 126 | filter_kind: Type of filter to add 127 | filter_settings: Settings object to initialize the filter with 128 | """ 129 | payload = { 130 | "sourceName": source_name, 131 | "filterName": filter_name, 132 | "filterKind": filter_kind 133 | } 134 | 135 | if filter_settings: 136 | payload["filterSettings"] = filter_settings 137 | 138 | await obs_client.send_request("CreateSourceFilter", payload) -------------------------------------------------------------------------------- /docs/protocol_split/media_inputs.json: -------------------------------------------------------------------------------- 1 | { 2 | "requests": [ 3 | { 4 | "description": "Gets the status of a media input.\n\nMedia States:\n\n- `OBS_MEDIA_STATE_NONE`\n- `OBS_MEDIA_STATE_PLAYING`\n- `OBS_MEDIA_STATE_OPENING`\n- `OBS_MEDIA_STATE_BUFFERING`\n- `OBS_MEDIA_STATE_PAUSED`\n- `OBS_MEDIA_STATE_STOPPED`\n- `OBS_MEDIA_STATE_ENDED`\n- `OBS_MEDIA_STATE_ERROR`", 5 | "requestType": "GetMediaInputStatus", 6 | "complexity": 2, 7 | "rpcVersion": "1", 8 | "deprecated": false, 9 | "initialVersion": "5.0.0", 10 | "category": "media inputs", 11 | "requestFields": [ 12 | { 13 | "valueName": "inputName", 14 | "valueType": "String", 15 | "valueDescription": "Name of the media input", 16 | "valueRestrictions": null, 17 | "valueOptional": true, 18 | "valueOptionalBehavior": "Unknown" 19 | }, 20 | { 21 | "valueName": "inputUuid", 22 | "valueType": "String", 23 | "valueDescription": "UUID of the media input", 24 | "valueRestrictions": null, 25 | "valueOptional": true, 26 | "valueOptionalBehavior": "Unknown" 27 | } 28 | ], 29 | "responseFields": [ 30 | { 31 | "valueName": "mediaState", 32 | "valueType": "String", 33 | "valueDescription": "State of the media input" 34 | }, 35 | { 36 | "valueName": "mediaDuration", 37 | "valueType": "Number", 38 | "valueDescription": "Total duration of the playing media in milliseconds. `null` if not playing" 39 | }, 40 | { 41 | "valueName": "mediaCursor", 42 | "valueType": "Number", 43 | "valueDescription": "Position of the cursor in milliseconds. `null` if not playing" 44 | } 45 | ] 46 | }, 47 | { 48 | "description": "Sets the cursor position of a media input.\n\nThis request does not perform bounds checking of the cursor position.", 49 | "requestType": "SetMediaInputCursor", 50 | "complexity": 2, 51 | "rpcVersion": "1", 52 | "deprecated": false, 53 | "initialVersion": "5.0.0", 54 | "category": "media inputs", 55 | "requestFields": [ 56 | { 57 | "valueName": "inputName", 58 | "valueType": "String", 59 | "valueDescription": "Name of the media input", 60 | "valueRestrictions": null, 61 | "valueOptional": true, 62 | "valueOptionalBehavior": "Unknown" 63 | }, 64 | { 65 | "valueName": "inputUuid", 66 | "valueType": "String", 67 | "valueDescription": "UUID of the media input", 68 | "valueRestrictions": null, 69 | "valueOptional": true, 70 | "valueOptionalBehavior": "Unknown" 71 | }, 72 | { 73 | "valueName": "mediaCursor", 74 | "valueType": "Number", 75 | "valueDescription": "New cursor position to set", 76 | "valueRestrictions": ">= 0", 77 | "valueOptional": false, 78 | "valueOptionalBehavior": null 79 | } 80 | ], 81 | "responseFields": [] 82 | }, 83 | { 84 | "description": "Offsets the current cursor position of a media input by the specified value.\n\nThis request does not perform bounds checking of the cursor position.", 85 | "requestType": "OffsetMediaInputCursor", 86 | "complexity": 2, 87 | "rpcVersion": "1", 88 | "deprecated": false, 89 | "initialVersion": "5.0.0", 90 | "category": "media inputs", 91 | "requestFields": [ 92 | { 93 | "valueName": "inputName", 94 | "valueType": "String", 95 | "valueDescription": "Name of the media input", 96 | "valueRestrictions": null, 97 | "valueOptional": true, 98 | "valueOptionalBehavior": "Unknown" 99 | }, 100 | { 101 | "valueName": "inputUuid", 102 | "valueType": "String", 103 | "valueDescription": "UUID of the media input", 104 | "valueRestrictions": null, 105 | "valueOptional": true, 106 | "valueOptionalBehavior": "Unknown" 107 | }, 108 | { 109 | "valueName": "mediaCursorOffset", 110 | "valueType": "Number", 111 | "valueDescription": "Value to offset the current cursor position by", 112 | "valueRestrictions": null, 113 | "valueOptional": false, 114 | "valueOptionalBehavior": null 115 | } 116 | ], 117 | "responseFields": [] 118 | }, 119 | { 120 | "description": "Triggers an action on a media input.", 121 | "requestType": "TriggerMediaInputAction", 122 | "complexity": 2, 123 | "rpcVersion": "1", 124 | "deprecated": false, 125 | "initialVersion": "5.0.0", 126 | "category": "media inputs", 127 | "requestFields": [ 128 | { 129 | "valueName": "inputName", 130 | "valueType": "String", 131 | "valueDescription": "Name of the media input", 132 | "valueRestrictions": null, 133 | "valueOptional": true, 134 | "valueOptionalBehavior": "Unknown" 135 | }, 136 | { 137 | "valueName": "inputUuid", 138 | "valueType": "String", 139 | "valueDescription": "UUID of the media input", 140 | "valueRestrictions": null, 141 | "valueOptional": true, 142 | "valueOptionalBehavior": "Unknown" 143 | }, 144 | { 145 | "valueName": "mediaAction", 146 | "valueType": "String", 147 | "valueDescription": "Identifier of the `ObsMediaInputAction` enum", 148 | "valueRestrictions": null, 149 | "valueOptional": false, 150 | "valueOptionalBehavior": null 151 | } 152 | ], 153 | "responseFields": [] 154 | } 155 | ] 156 | } -------------------------------------------------------------------------------- /py_src/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import json 5 | import asyncio 6 | import websockets 7 | import logging 8 | from typing import Any, Dict, List, Optional, Union 9 | 10 | OBS_WS_URL = "ws://localhost:4455" 11 | OBS_WS_PASSWORD = os.environ.get("OBS_WS_PASSWORD", "") 12 | 13 | # Setup logging 14 | logger = logging.getLogger("obs_client") 15 | 16 | class OBSWebSocketClient: 17 | def __init__(self, url: str = OBS_WS_URL, password: str = OBS_WS_PASSWORD, loop=None): 18 | self.url = url 19 | self.password = password 20 | self.ws = None 21 | self.message_id = 0 22 | self.authenticated = False 23 | self.loop = loop or asyncio.get_event_loop() 24 | self.lock = asyncio.Lock() 25 | 26 | async def connect(self): 27 | """Connect to OBS WebSocket server""" 28 | if self.ws: 29 | try: 30 | # Try a ping to see if connection is still alive 31 | pong = await self.ws.ping() 32 | await asyncio.wait_for(pong, timeout=2.0) 33 | return # Connection is still good 34 | except Exception: 35 | # Connection is stale, close it and reconnect 36 | logger.info("Connection stale, reconnecting...") 37 | try: 38 | await self.ws.close() 39 | except Exception: 40 | pass 41 | self.ws = None 42 | self.authenticated = False 43 | 44 | try: 45 | logger.info(f"Connecting to OBS WebSocket at {self.url}") 46 | self.ws = await websockets.connect(self.url) 47 | await self._authenticate() 48 | logger.info("Successfully connected to OBS WebSocket server") 49 | except Exception as e: 50 | self.ws = None 51 | self.authenticated = False 52 | logger.error(f"Failed to connect to OBS WebSocket server: {e}") 53 | raise Exception(f"Failed to connect to OBS WebSocket server: {e}") 54 | 55 | async def _authenticate(self): 56 | """Authenticate with OBS WebSocket server""" 57 | # Receive hello message first 58 | hello = await self.ws.recv() 59 | hello_data = json.loads(hello) 60 | logger.debug(f"Received hello: {hello_data}") 61 | 62 | if hello_data["op"] != 0: # Hello op code 63 | raise Exception("Did not receive Hello message from OBS WebSocket server") 64 | 65 | auth_data = { 66 | "op": 1, # Identify op code 67 | "d": { 68 | "rpcVersion": 1, 69 | "authentication": self.password, 70 | "eventSubscriptions": 0 # We don't need events for now 71 | } 72 | } 73 | 74 | logger.debug("Sending authentication...") 75 | await self.ws.send(json.dumps(auth_data)) 76 | response = await self.ws.recv() 77 | response_data = json.loads(response) 78 | logger.debug(f"Received auth response: {response_data}") 79 | 80 | if response_data["op"] != 2: # Identified op code 81 | raise Exception("Authentication failed") 82 | 83 | self.authenticated = True 84 | logger.info("Successfully authenticated with OBS WebSocket server") 85 | 86 | async def send_request(self, request_type: str, request_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: 87 | """Send a request to OBS WebSocket server and wait for response""" 88 | if not self.ws or not self.authenticated: 89 | await self.connect() 90 | 91 | request_id = str(self.message_id) 92 | self.message_id += 1 93 | 94 | payload = { 95 | "op": 6, # Request op code 96 | "d": { 97 | "requestType": request_type, 98 | "requestId": request_id 99 | } 100 | } 101 | 102 | if request_data: 103 | payload["d"]["requestData"] = request_data 104 | 105 | logger.debug(f"Sending request {request_type} (ID: {request_id})") 106 | await self.ws.send(json.dumps(payload)) 107 | 108 | # Wait for response with timeout 109 | start_time = self.loop.time() 110 | timeout = 5.0 # 5 seconds timeout 111 | 112 | while True: 113 | if self.loop.time() - start_time > timeout: 114 | logger.error(f"Timeout waiting for response to {request_type}") 115 | raise Exception(f"Timeout waiting for OBS WebSocket response for {request_type}") 116 | 117 | try: 118 | # Wait for response with small timeout to allow for cancellation 119 | response = await asyncio.wait_for(self.ws.recv(), 0.5) 120 | response_data = json.loads(response) 121 | 122 | # Check if this is our response 123 | if response_data["op"] == 7: # RequestResponse op code 124 | resp_id = response_data["d"]["requestId"] 125 | 126 | if resp_id == request_id: 127 | # Check status 128 | status = response_data["d"]["requestStatus"] 129 | if not status["result"]: 130 | error = status.get("comment", "Unknown error") 131 | logger.error(f"Request {request_type} failed: {error}") 132 | raise Exception(f"OBS WebSocket request failed: {error}") 133 | 134 | logger.debug(f"Received response for {request_type}") 135 | return response_data["d"].get("responseData", {}) 136 | except asyncio.TimeoutError: 137 | # Just continue waiting 138 | continue 139 | except Exception as e: 140 | if not isinstance(e, asyncio.TimeoutError): # We handle timeout explicitly above 141 | logger.error(f"Error waiting for response: {e}") 142 | raise Exception(f"Error communicating with OBS WebSocket: {e}") 143 | 144 | async def close(self): 145 | """Close the connection to OBS WebSocket server""" 146 | if self.ws: 147 | logger.info("Closing connection to OBS WebSocket server") 148 | await self.ws.close() 149 | self.ws = None 150 | self.authenticated = False 151 | 152 | # Don't create a singleton client here - it will be created in server.py 153 | # obs_client = OBSWebSocketClient() -------------------------------------------------------------------------------- /py_src/streaming.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from typing import Any, Dict, Optional 4 | 5 | from .client import obs_client 6 | from .server import mcp 7 | 8 | @mcp.tool() 9 | async def get_stream_status() -> Dict[str, Any]: 10 | """ 11 | Gets the status of the stream output. 12 | 13 | Returns: 14 | Dict containing stream status information including: 15 | - outputActive: Whether the output is active 16 | - outputReconnecting: Whether the output is reconnecting 17 | - outputTimecode: Timecode string of the output 18 | - outputDuration: Duration in milliseconds of the output 19 | - outputBytes: Total bytes sent by the output 20 | - outputSkippedFrames: Number of frames skipped by the output 21 | - outputTotalFrames: Total frames processed by the output 22 | """ 23 | return await obs_client.send_request("GetStreamStatus") 24 | 25 | @mcp.tool() 26 | async def toggle_stream() -> bool: 27 | """ 28 | Toggles the status of the stream output. 29 | 30 | Returns: 31 | Whether the output is active after toggling 32 | """ 33 | response = await obs_client.send_request("ToggleStream") 34 | return response.get("outputActive", False) 35 | 36 | @mcp.tool() 37 | async def start_stream() -> None: 38 | """ 39 | Starts the stream output. 40 | """ 41 | await obs_client.send_request("StartStream") 42 | 43 | @mcp.tool() 44 | async def stop_stream() -> None: 45 | """ 46 | Stops the stream output. 47 | """ 48 | await obs_client.send_request("StopStream") 49 | 50 | @mcp.tool() 51 | async def send_stream_caption(caption_text: str) -> None: 52 | """ 53 | Sends CEA-608 caption text over the stream output. 54 | 55 | Args: 56 | caption_text: Caption text to send 57 | """ 58 | await obs_client.send_request("SendStreamCaption", {"captionText": caption_text}) 59 | 60 | @mcp.tool() 61 | async def get_record_status() -> Dict[str, Any]: 62 | """ 63 | Gets the status of the record output. 64 | 65 | Returns: 66 | Dict containing record status information including: 67 | - outputActive: Whether the output is active 68 | - outputPaused: Whether the output is paused 69 | - outputTimecode: Timecode string of the output 70 | - outputDuration: Duration in milliseconds of the output 71 | - outputBytes: Total bytes recorded 72 | - outputPath: File path of the recording 73 | """ 74 | return await obs_client.send_request("GetRecordStatus") 75 | 76 | @mcp.tool() 77 | async def toggle_record() -> bool: 78 | """ 79 | Toggles the status of the record output. 80 | 81 | Returns: 82 | Whether the output is active after toggling 83 | """ 84 | response = await obs_client.send_request("ToggleRecord") 85 | return response.get("outputActive", False) 86 | 87 | @mcp.tool() 88 | async def start_record() -> None: 89 | """ 90 | Starts the record output. 91 | """ 92 | await obs_client.send_request("StartRecord") 93 | 94 | @mcp.tool() 95 | async def stop_record() -> Dict[str, str]: 96 | """ 97 | Stops the record output. 98 | 99 | Returns: 100 | Dict containing the output path of the stopped recording 101 | """ 102 | return await obs_client.send_request("StopRecord") 103 | 104 | @mcp.tool() 105 | async def toggle_record_pause() -> bool: 106 | """ 107 | Toggles pause on the record output. 108 | 109 | Returns: 110 | Whether the output is paused after toggling 111 | """ 112 | response = await obs_client.send_request("ToggleRecordPause") 113 | return response.get("outputPaused", False) 114 | 115 | @mcp.tool() 116 | async def pause_record() -> None: 117 | """ 118 | Pauses the record output. 119 | """ 120 | await obs_client.send_request("PauseRecord") 121 | 122 | @mcp.tool() 123 | async def resume_record() -> None: 124 | """ 125 | Resumes the record output. 126 | """ 127 | await obs_client.send_request("ResumeRecord") 128 | 129 | @mcp.tool() 130 | async def get_virtual_cam_status() -> Dict[str, Any]: 131 | """ 132 | Gets the status of the virtual camera output. 133 | 134 | Returns: 135 | Dict containing: 136 | - outputActive: Whether the output is active 137 | """ 138 | return await obs_client.send_request("GetVirtualCamStatus") 139 | 140 | @mcp.tool() 141 | async def toggle_virtual_cam() -> bool: 142 | """ 143 | Toggles the state of the virtual camera output. 144 | 145 | Returns: 146 | Whether the output is active after toggling 147 | """ 148 | response = await obs_client.send_request("ToggleVirtualCam") 149 | return response.get("outputActive", False) 150 | 151 | @mcp.tool() 152 | async def start_virtual_cam() -> None: 153 | """ 154 | Starts the virtual camera output. 155 | """ 156 | await obs_client.send_request("StartVirtualCam") 157 | 158 | @mcp.tool() 159 | async def stop_virtual_cam() -> None: 160 | """ 161 | Stops the virtual camera output. 162 | """ 163 | await obs_client.send_request("StopVirtualCam") 164 | 165 | @mcp.tool() 166 | async def get_replay_buffer_status() -> Dict[str, Any]: 167 | """ 168 | Gets the status of the replay buffer output. 169 | 170 | Returns: 171 | Dict containing: 172 | - outputActive: Whether the output is active 173 | """ 174 | return await obs_client.send_request("GetReplayBufferStatus") 175 | 176 | @mcp.tool() 177 | async def toggle_replay_buffer() -> bool: 178 | """ 179 | Toggles the state of the replay buffer output. 180 | 181 | Returns: 182 | Whether the output is active after toggling 183 | """ 184 | response = await obs_client.send_request("ToggleReplayBuffer") 185 | return response.get("outputActive", False) 186 | 187 | @mcp.tool() 188 | async def start_replay_buffer() -> None: 189 | """ 190 | Starts the replay buffer output. 191 | """ 192 | await obs_client.send_request("StartReplayBuffer") 193 | 194 | @mcp.tool() 195 | async def stop_replay_buffer() -> None: 196 | """ 197 | Stops the replay buffer output. 198 | """ 199 | await obs_client.send_request("StopReplayBuffer") 200 | 201 | @mcp.tool() 202 | async def save_replay_buffer() -> None: 203 | """ 204 | Saves the contents of the replay buffer output. 205 | """ 206 | await obs_client.send_request("SaveReplayBuffer") 207 | 208 | @mcp.tool() 209 | async def get_last_replay_buffer_replay() -> Dict[str, str]: 210 | """ 211 | Gets the filename of the last replay buffer save file. 212 | 213 | Returns: 214 | Dict containing: 215 | - savedReplayPath: Path of the saved replay file 216 | """ 217 | return await obs_client.send_request("GetLastReplayBufferReplay") -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { OBSWebSocketClient } from "./client.js"; 4 | import * as tools from "./tools/index.js"; 5 | 6 | // Create the OBS WebSocket client 7 | const obsClient = new OBSWebSocketClient( 8 | process.env.OBS_WEBSOCKET_URL || "ws://localhost:4455", 9 | process.env.OBS_WEBSOCKET_PASSWORD || null 10 | ); 11 | 12 | // Create the MCP server 13 | export const server = new McpServer({ 14 | name: "obs-mcp", 15 | version: "1.0.0", 16 | }); 17 | 18 | export let serverConnected = false; 19 | export let obsConnected = false; 20 | let reconnectInterval: NodeJS.Timeout | null = null; 21 | let connectionCheckInterval: NodeJS.Timeout | null = null; 22 | let reconnectAttempts = 0; 23 | const RECONNECT_INTERVAL = 5000; // 5 seconds (reduced from 10) 24 | const CONNECTION_CHECK_INTERVAL = 1000; // 1 second (reduced from 5) 25 | const MAX_BACKOFF_INTERVAL = 30000; // Max 30 seconds between attempts 26 | 27 | const logger = { 28 | log: (message: string) => console.error(message), 29 | error: (message: string) => console.error(message), 30 | debug: (message: string) => console.error(message), 31 | }; 32 | 33 | // Function to attempt OBS connection 34 | async function attemptOBSConnection(): Promise { 35 | try { 36 | logger.log("Attempting to connect to OBS WebSocket..."); 37 | 38 | // Set a timeout for the connection attempt 39 | const connectionPromise = obsClient.connect(); 40 | const timeoutPromise = new Promise((_, reject) => { 41 | setTimeout(() => reject(new Error("Connection timeout after 10 seconds")), 10000); 42 | }); 43 | 44 | await Promise.race([connectionPromise, timeoutPromise]); 45 | logger.log("Connected to OBS WebSocket server"); 46 | obsConnected = true; 47 | reconnectAttempts = 0; 48 | 49 | // Clear any existing reconnect interval 50 | if (reconnectInterval) { 51 | clearInterval(reconnectInterval); 52 | reconnectInterval = null; 53 | } 54 | 55 | // Set up disconnect handler to trigger reconnection 56 | obsClient.on('disconnected', () => { 57 | logger.log("OBS WebSocket disconnected, will attempt to reconnect..."); 58 | obsConnected = false; 59 | startReconnectionTimer(); 60 | }); 61 | 62 | } catch (obsError) { 63 | const errorMessage = obsError instanceof Error ? obsError.message : String(obsError); 64 | logger.error(`Failed to connect to OBS WebSocket: ${errorMessage}`); 65 | 66 | if (reconnectAttempts === 0) { 67 | logger.error("The server will continue running without OBS connection."); 68 | logger.error("Make sure OBS Studio is running with WebSocket enabled on port 4455"); 69 | logger.error("You can also set OBS_WEBSOCKET_URL and OBS_WEBSOCKET_PASSWORD environment variables"); 70 | logger.error("The server will attempt to reconnect every 5 seconds..."); 71 | } 72 | 73 | obsConnected = false; 74 | reconnectAttempts++; 75 | 76 | // Use exponential backoff with a maximum interval 77 | const backoffInterval = Math.min(RECONNECT_INTERVAL * Math.pow(1.5, Math.min(reconnectAttempts, 3)), MAX_BACKOFF_INTERVAL); 78 | startReconnectionTimer(backoffInterval); 79 | } 80 | } 81 | 82 | // Function to start reconnection timer with backoff 83 | function startReconnectionTimer(interval?: number): void { 84 | if (reconnectInterval) { 85 | clearInterval(reconnectInterval); 86 | } 87 | 88 | const checkInterval = interval || RECONNECT_INTERVAL; 89 | logger.debug(`Will retry connection in ${checkInterval / 1000} seconds...`); 90 | 91 | reconnectInterval = setInterval(async () => { 92 | if (!obsConnected) { 93 | logger.log(`Reconnection attempt ${reconnectAttempts + 1}...`); 94 | await attemptOBSConnection(); 95 | } 96 | }, checkInterval); 97 | } 98 | 99 | // Function to start periodic connection checking 100 | function startConnectionCheckTimer(): void { 101 | if (connectionCheckInterval) { 102 | clearInterval(connectionCheckInterval); 103 | } 104 | 105 | connectionCheckInterval = setInterval(async () => { 106 | // Only check if we're not currently connected 107 | if (!obsConnected) { 108 | logger.debug("Checking if OBS is now available..."); 109 | try { 110 | // Try a quick connection test 111 | await attemptOBSConnection(); 112 | if (obsConnected) { 113 | logger.log("🎉 OBS became available and connection was established!"); 114 | } 115 | } catch (error) { 116 | // Silently fail - this is just a check, not a retry 117 | logger.debug("OBS still not available"); 118 | } 119 | } 120 | }, CONNECTION_CHECK_INTERVAL); 121 | } 122 | 123 | // Set up server startup logic 124 | export async function startServer() { 125 | try { 126 | // Initialize all tools with the OBS client 127 | await tools.initialize(server, obsClient); 128 | logger.log("Initialized MCP tools"); 129 | 130 | // Connect the MCP server to stdio transport 131 | const transport = new StdioServerTransport(); 132 | await server.connect(transport); 133 | logger.log("OBS MCP Server running on stdio"); 134 | 135 | serverConnected = true; 136 | 137 | // Try to connect to OBS WebSocket (but don't fail if it's not available) 138 | await attemptOBSConnection(); 139 | 140 | // Start the periodic connection check timer 141 | startConnectionCheckTimer(); 142 | 143 | // Set up graceful shutdown 144 | process.on("SIGINT", handleShutdown); 145 | process.on("SIGTERM", handleShutdown); 146 | 147 | logger.log("Server startup complete"); 148 | 149 | // Log connection status 150 | if (obsConnected) { 151 | logger.log("✅ OBS WebSocket: Connected"); 152 | } else { 153 | logger.log("❌ OBS WebSocket: Disconnected (will retry automatically)"); 154 | logger.log("💡 The server will also check every 1 second if OBS becomes available"); 155 | } 156 | 157 | } catch (error) { 158 | const errorMessage = error instanceof Error ? error.message : String(error); 159 | logger.error(`Error starting server: ${errorMessage}`); 160 | if (error instanceof Error && error.stack) { 161 | logger.error(`Stack trace: ${error.stack}`); 162 | } 163 | process.exit(1); 164 | } 165 | } 166 | 167 | // Handle graceful shutdown 168 | async function handleShutdown() { 169 | logger.log("Shutting down..."); 170 | 171 | // Clear all intervals 172 | if (reconnectInterval) { 173 | clearInterval(reconnectInterval); 174 | reconnectInterval = null; 175 | } 176 | 177 | if (connectionCheckInterval) { 178 | clearInterval(connectionCheckInterval); 179 | connectionCheckInterval = null; 180 | } 181 | 182 | // Disconnect from OBS if connected 183 | if (obsConnected) { 184 | obsClient.disconnect(); 185 | } 186 | 187 | process.exit(0); 188 | } 189 | 190 | export { obsClient }; -------------------------------------------------------------------------------- /src/tools/scenes.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { OBSWebSocketClient } from "../client.js"; 3 | import { z } from "zod"; 4 | 5 | export async function initialize(server: McpServer, client: OBSWebSocketClient): Promise { 6 | // GetSceneList tool 7 | server.tool( 8 | "obs-get-scene-list", 9 | "Get a list of scenes in OBS", 10 | {}, 11 | async () => { 12 | try { 13 | const sceneList = await client.sendRequest("GetSceneList"); 14 | return { 15 | content: [ 16 | { 17 | type: "text", 18 | text: JSON.stringify(sceneList, null, 2) 19 | } 20 | ] 21 | }; 22 | } catch (error) { 23 | return { 24 | content: [ 25 | { 26 | type: "text", 27 | text: `Error getting scene list: ${error instanceof Error ? error.message : String(error)}` 28 | } 29 | ], 30 | isError: true 31 | }; 32 | } 33 | } 34 | ); 35 | 36 | // GetCurrentProgramScene tool 37 | server.tool( 38 | "obs-get-current-scene", 39 | "Get the current active scene in OBS", 40 | {}, 41 | async () => { 42 | try { 43 | const currentScene = await client.sendRequest("GetCurrentProgramScene"); 44 | return { 45 | content: [ 46 | { 47 | type: "text", 48 | text: `Current scene: ${currentScene.currentProgramSceneName}` 49 | } 50 | ] 51 | }; 52 | } catch (error) { 53 | return { 54 | content: [ 55 | { 56 | type: "text", 57 | text: `Error getting current scene: ${error instanceof Error ? error.message : String(error)}` 58 | } 59 | ], 60 | isError: true 61 | }; 62 | } 63 | } 64 | ); 65 | 66 | // SetCurrentProgramScene tool 67 | server.tool( 68 | "obs-set-current-scene", 69 | "Set the current active scene in OBS", 70 | { 71 | sceneName: z.string().describe("The name of the scene to set as current") 72 | }, 73 | async ({ sceneName }) => { 74 | try { 75 | await client.sendRequest("SetCurrentProgramScene", { sceneName }); 76 | return { 77 | content: [ 78 | { 79 | type: "text", 80 | text: `Successfully switched to scene: ${sceneName}` 81 | } 82 | ] 83 | }; 84 | } catch (error) { 85 | return { 86 | content: [ 87 | { 88 | type: "text", 89 | text: `Error setting current scene: ${error instanceof Error ? error.message : String(error)}` 90 | } 91 | ], 92 | isError: true 93 | }; 94 | } 95 | } 96 | ); 97 | 98 | // GetCurrentPreviewScene tool (Studio Mode) 99 | server.tool( 100 | "obs-get-preview-scene", 101 | "Get the current preview scene in OBS Studio Mode", 102 | {}, 103 | async () => { 104 | try { 105 | const previewScene = await client.sendRequest("GetCurrentPreviewScene"); 106 | return { 107 | content: [ 108 | { 109 | type: "text", 110 | text: `Preview scene: ${previewScene.currentPreviewSceneName}` 111 | } 112 | ] 113 | }; 114 | } catch (error) { 115 | return { 116 | content: [ 117 | { 118 | type: "text", 119 | text: `Error getting preview scene: ${error instanceof Error ? error.message : String(error)}` 120 | } 121 | ], 122 | isError: true 123 | }; 124 | } 125 | } 126 | ); 127 | 128 | // SetCurrentPreviewScene tool (Studio Mode) 129 | server.tool( 130 | "obs-set-preview-scene", 131 | "Set the current preview scene in OBS Studio Mode", 132 | { 133 | sceneName: z.string().describe("The name of the scene to set as preview") 134 | }, 135 | async ({ sceneName }) => { 136 | try { 137 | await client.sendRequest("SetCurrentPreviewScene", { sceneName }); 138 | return { 139 | content: [ 140 | { 141 | type: "text", 142 | text: `Successfully set preview scene to: ${sceneName}` 143 | } 144 | ] 145 | }; 146 | } catch (error) { 147 | return { 148 | content: [ 149 | { 150 | type: "text", 151 | text: `Error setting preview scene: ${error instanceof Error ? error.message : String(error)}` 152 | } 153 | ], 154 | isError: true 155 | }; 156 | } 157 | } 158 | ); 159 | 160 | // CreateScene tool 161 | server.tool( 162 | "obs-create-scene", 163 | "Create a new scene in OBS", 164 | { 165 | sceneName: z.string().describe("The name for the new scene") 166 | }, 167 | async ({ sceneName }) => { 168 | try { 169 | await client.sendRequest("CreateScene", { sceneName }); 170 | return { 171 | content: [ 172 | { 173 | type: "text", 174 | text: `Successfully created scene: ${sceneName}` 175 | } 176 | ] 177 | }; 178 | } catch (error) { 179 | return { 180 | content: [ 181 | { 182 | type: "text", 183 | text: `Error creating scene: ${error instanceof Error ? error.message : String(error)}` 184 | } 185 | ], 186 | isError: true 187 | }; 188 | } 189 | } 190 | ); 191 | 192 | // RemoveScene tool 193 | server.tool( 194 | "obs-remove-scene", 195 | "Remove a scene from OBS", 196 | { 197 | sceneName: z.string().describe("The name of the scene to remove") 198 | }, 199 | async ({ sceneName }) => { 200 | try { 201 | await client.sendRequest("RemoveScene", { sceneName }); 202 | return { 203 | content: [ 204 | { 205 | type: "text", 206 | text: `Successfully removed scene: ${sceneName}` 207 | } 208 | ] 209 | }; 210 | } catch (error) { 211 | return { 212 | content: [ 213 | { 214 | type: "text", 215 | text: `Error removing scene: ${error instanceof Error ? error.message : String(error)}` 216 | } 217 | ], 218 | isError: true 219 | }; 220 | } 221 | } 222 | ); 223 | 224 | // TriggerStudioModeTransition tool 225 | server.tool( 226 | "obs-trigger-studio-transition", 227 | "Trigger a transition from preview to program scene in Studio Mode", 228 | {}, 229 | async () => { 230 | try { 231 | await client.sendRequest("TriggerStudioModeTransition"); 232 | return { 233 | content: [ 234 | { 235 | type: "text", 236 | text: "Successfully triggered studio mode transition" 237 | } 238 | ] 239 | }; 240 | } catch (error) { 241 | return { 242 | content: [ 243 | { 244 | type: "text", 245 | text: `Error triggering studio transition: ${error instanceof Error ? error.message : String(error)}` 246 | } 247 | ], 248 | isError: true 249 | }; 250 | } 251 | } 252 | ); 253 | } -------------------------------------------------------------------------------- /docs/protocol_split/sources.json: -------------------------------------------------------------------------------- 1 | { 2 | "requests": [ 3 | { 4 | "description": "Gets the active and show state of a source.\n\n**Compatible with inputs and scenes.**", 5 | "requestType": "GetSourceActive", 6 | "complexity": 2, 7 | "rpcVersion": "1", 8 | "deprecated": false, 9 | "initialVersion": "5.0.0", 10 | "category": "sources", 11 | "requestFields": [ 12 | { 13 | "valueName": "sourceName", 14 | "valueType": "String", 15 | "valueDescription": "Name of the source to get the active state of", 16 | "valueRestrictions": null, 17 | "valueOptional": true, 18 | "valueOptionalBehavior": "Unknown" 19 | }, 20 | { 21 | "valueName": "sourceUuid", 22 | "valueType": "String", 23 | "valueDescription": "UUID of the source to get the active state of", 24 | "valueRestrictions": null, 25 | "valueOptional": true, 26 | "valueOptionalBehavior": "Unknown" 27 | } 28 | ], 29 | "responseFields": [ 30 | { 31 | "valueName": "videoActive", 32 | "valueType": "Boolean", 33 | "valueDescription": "Whether the source is showing in Program" 34 | }, 35 | { 36 | "valueName": "videoShowing", 37 | "valueType": "Boolean", 38 | "valueDescription": "Whether the source is showing in the UI (Preview, Projector, Properties)" 39 | } 40 | ] 41 | }, 42 | { 43 | "description": "Gets a Base64-encoded screenshot of a source.\n\nThe `imageWidth` and `imageHeight` parameters are treated as \"scale to inner\", meaning the smallest ratio will be used and the aspect ratio of the original resolution is kept.\nIf `imageWidth` and `imageHeight` are not specified, the compressed image will use the full resolution of the source.\n\n**Compatible with inputs and scenes.**", 44 | "requestType": "GetSourceScreenshot", 45 | "complexity": 4, 46 | "rpcVersion": "1", 47 | "deprecated": false, 48 | "initialVersion": "5.0.0", 49 | "category": "sources", 50 | "requestFields": [ 51 | { 52 | "valueName": "sourceName", 53 | "valueType": "String", 54 | "valueDescription": "Name of the source to take a screenshot of", 55 | "valueRestrictions": null, 56 | "valueOptional": true, 57 | "valueOptionalBehavior": "Unknown" 58 | }, 59 | { 60 | "valueName": "sourceUuid", 61 | "valueType": "String", 62 | "valueDescription": "UUID of the source to take a screenshot of", 63 | "valueRestrictions": null, 64 | "valueOptional": true, 65 | "valueOptionalBehavior": "Unknown" 66 | }, 67 | { 68 | "valueName": "imageFormat", 69 | "valueType": "String", 70 | "valueDescription": "Image compression format to use. Use `GetVersion` to get compatible image formats", 71 | "valueRestrictions": null, 72 | "valueOptional": false, 73 | "valueOptionalBehavior": null 74 | }, 75 | { 76 | "valueName": "imageWidth", 77 | "valueType": "Number", 78 | "valueDescription": "Width to scale the screenshot to", 79 | "valueRestrictions": ">= 8, <= 4096", 80 | "valueOptional": true, 81 | "valueOptionalBehavior": "Source value is used" 82 | }, 83 | { 84 | "valueName": "imageHeight", 85 | "valueType": "Number", 86 | "valueDescription": "Height to scale the screenshot to", 87 | "valueRestrictions": ">= 8, <= 4096", 88 | "valueOptional": true, 89 | "valueOptionalBehavior": "Source value is used" 90 | }, 91 | { 92 | "valueName": "imageCompressionQuality", 93 | "valueType": "Number", 94 | "valueDescription": "Compression quality to use. 0 for high compression, 100 for uncompressed. -1 to use \"default\" (whatever that means, idk)", 95 | "valueRestrictions": ">= -1, <= 100", 96 | "valueOptional": true, 97 | "valueOptionalBehavior": "-1" 98 | } 99 | ], 100 | "responseFields": [ 101 | { 102 | "valueName": "imageData", 103 | "valueType": "String", 104 | "valueDescription": "Base64-encoded screenshot" 105 | } 106 | ] 107 | }, 108 | { 109 | "description": "Saves a screenshot of a source to the filesystem.\n\nThe `imageWidth` and `imageHeight` parameters are treated as \"scale to inner\", meaning the smallest ratio will be used and the aspect ratio of the original resolution is kept.\nIf `imageWidth` and `imageHeight` are not specified, the compressed image will use the full resolution of the source.\n\n**Compatible with inputs and scenes.**", 110 | "requestType": "SaveSourceScreenshot", 111 | "complexity": 3, 112 | "rpcVersion": "1", 113 | "deprecated": false, 114 | "initialVersion": "5.0.0", 115 | "category": "sources", 116 | "requestFields": [ 117 | { 118 | "valueName": "sourceName", 119 | "valueType": "String", 120 | "valueDescription": "Name of the source to take a screenshot of", 121 | "valueRestrictions": null, 122 | "valueOptional": true, 123 | "valueOptionalBehavior": "Unknown" 124 | }, 125 | { 126 | "valueName": "sourceUuid", 127 | "valueType": "String", 128 | "valueDescription": "UUID of the source to take a screenshot of", 129 | "valueRestrictions": null, 130 | "valueOptional": true, 131 | "valueOptionalBehavior": "Unknown" 132 | }, 133 | { 134 | "valueName": "imageFormat", 135 | "valueType": "String", 136 | "valueDescription": "Image compression format to use. Use `GetVersion` to get compatible image formats", 137 | "valueRestrictions": null, 138 | "valueOptional": false, 139 | "valueOptionalBehavior": null 140 | }, 141 | { 142 | "valueName": "imageFilePath", 143 | "valueType": "String", 144 | "valueDescription": "Path to save the screenshot file to. Eg. `C:\\Users\\user\\Desktop\\screenshot.png`", 145 | "valueRestrictions": null, 146 | "valueOptional": false, 147 | "valueOptionalBehavior": null 148 | }, 149 | { 150 | "valueName": "imageWidth", 151 | "valueType": "Number", 152 | "valueDescription": "Width to scale the screenshot to", 153 | "valueRestrictions": ">= 8, <= 4096", 154 | "valueOptional": true, 155 | "valueOptionalBehavior": "Source value is used" 156 | }, 157 | { 158 | "valueName": "imageHeight", 159 | "valueType": "Number", 160 | "valueDescription": "Height to scale the screenshot to", 161 | "valueRestrictions": ">= 8, <= 4096", 162 | "valueOptional": true, 163 | "valueOptionalBehavior": "Source value is used" 164 | }, 165 | { 166 | "valueName": "imageCompressionQuality", 167 | "valueType": "Number", 168 | "valueDescription": "Compression quality to use. 0 for high compression, 100 for uncompressed. -1 to use \"default\" (whatever that means, idk)", 169 | "valueRestrictions": ">= -1, <= 100", 170 | "valueOptional": true, 171 | "valueOptionalBehavior": "-1" 172 | } 173 | ], 174 | "responseFields": [] 175 | } 176 | ] 177 | } -------------------------------------------------------------------------------- /src/tools/record.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { OBSWebSocketClient } from "../client.js"; 3 | import { z } from "zod"; 4 | 5 | export async function initialize(server: McpServer, client: OBSWebSocketClient): Promise { 6 | // GetRecordStatus tool 7 | server.tool( 8 | "obs-get-record-status", 9 | "Gets the status of the record output", 10 | {}, 11 | async () => { 12 | try { 13 | const response = await client.sendRequest("GetRecordStatus"); 14 | return { 15 | content: [ 16 | { 17 | type: "text", 18 | text: JSON.stringify(response, null, 2) 19 | } 20 | ] 21 | }; 22 | } catch (error) { 23 | return { 24 | content: [ 25 | { 26 | type: "text", 27 | text: `Error getting record status: ${error instanceof Error ? error.message : String(error)}` 28 | } 29 | ], 30 | isError: true 31 | }; 32 | } 33 | } 34 | ); 35 | 36 | // ToggleRecord tool 37 | server.tool( 38 | "obs-toggle-record", 39 | "Toggles the status of the record output", 40 | {}, 41 | async () => { 42 | try { 43 | const response = await client.sendRequest("ToggleRecord"); 44 | return { 45 | content: [ 46 | { 47 | type: "text", 48 | text: `Recording toggled, now ${response.outputActive ? "active" : "inactive"}` 49 | } 50 | ] 51 | }; 52 | } catch (error) { 53 | return { 54 | content: [ 55 | { 56 | type: "text", 57 | text: `Error toggling recording: ${error instanceof Error ? error.message : String(error)}` 58 | } 59 | ], 60 | isError: true 61 | }; 62 | } 63 | } 64 | ); 65 | 66 | // StartRecord tool 67 | server.tool( 68 | "obs-start-record", 69 | "Starts the record output", 70 | {}, 71 | async () => { 72 | try { 73 | await client.sendRequest("StartRecord"); 74 | return { 75 | content: [ 76 | { 77 | type: "text", 78 | text: "Recording started" 79 | } 80 | ] 81 | }; 82 | } catch (error) { 83 | return { 84 | content: [ 85 | { 86 | type: "text", 87 | text: `Error starting recording: ${error instanceof Error ? error.message : String(error)}` 88 | } 89 | ], 90 | isError: true 91 | }; 92 | } 93 | } 94 | ); 95 | 96 | // StopRecord tool 97 | server.tool( 98 | "obs-stop-record", 99 | "Stops the record output", 100 | {}, 101 | async () => { 102 | try { 103 | const response = await client.sendRequest("StopRecord"); 104 | return { 105 | content: [ 106 | { 107 | type: "text", 108 | text: `Recording stopped, saved to: ${response.outputPath}` 109 | } 110 | ] 111 | }; 112 | } catch (error) { 113 | return { 114 | content: [ 115 | { 116 | type: "text", 117 | text: `Error stopping recording: ${error instanceof Error ? error.message : String(error)}` 118 | } 119 | ], 120 | isError: true 121 | }; 122 | } 123 | } 124 | ); 125 | 126 | // ToggleRecordPause tool 127 | server.tool( 128 | "obs-toggle-record-pause", 129 | "Toggles pause on the record output", 130 | {}, 131 | async () => { 132 | try { 133 | await client.sendRequest("ToggleRecordPause"); 134 | return { 135 | content: [ 136 | { 137 | type: "text", 138 | text: "Recording pause toggled" 139 | } 140 | ] 141 | }; 142 | } catch (error) { 143 | return { 144 | content: [ 145 | { 146 | type: "text", 147 | text: `Error toggling record pause: ${error instanceof Error ? error.message : String(error)}` 148 | } 149 | ], 150 | isError: true 151 | }; 152 | } 153 | } 154 | ); 155 | 156 | // PauseRecord tool 157 | server.tool( 158 | "obs-pause-record", 159 | "Pauses the record output", 160 | {}, 161 | async () => { 162 | try { 163 | await client.sendRequest("PauseRecord"); 164 | return { 165 | content: [ 166 | { 167 | type: "text", 168 | text: "Recording paused" 169 | } 170 | ] 171 | }; 172 | } catch (error) { 173 | return { 174 | content: [ 175 | { 176 | type: "text", 177 | text: `Error pausing recording: ${error instanceof Error ? error.message : String(error)}` 178 | } 179 | ], 180 | isError: true 181 | }; 182 | } 183 | } 184 | ); 185 | 186 | // ResumeRecord tool 187 | server.tool( 188 | "obs-resume-record", 189 | "Resumes the record output", 190 | {}, 191 | async () => { 192 | try { 193 | await client.sendRequest("ResumeRecord"); 194 | return { 195 | content: [ 196 | { 197 | type: "text", 198 | text: "Recording resumed" 199 | } 200 | ] 201 | }; 202 | } catch (error) { 203 | return { 204 | content: [ 205 | { 206 | type: "text", 207 | text: `Error resuming recording: ${error instanceof Error ? error.message : String(error)}` 208 | } 209 | ], 210 | isError: true 211 | }; 212 | } 213 | } 214 | ); 215 | 216 | // SplitRecordFile tool 217 | server.tool( 218 | "obs-split-record-file", 219 | "Splits the current file being recorded into a new file", 220 | {}, 221 | async () => { 222 | try { 223 | await client.sendRequest("SplitRecordFile"); 224 | return { 225 | content: [ 226 | { 227 | type: "text", 228 | text: "Recording file split" 229 | } 230 | ] 231 | }; 232 | } catch (error) { 233 | return { 234 | content: [ 235 | { 236 | type: "text", 237 | text: `Error splitting record file: ${error instanceof Error ? error.message : String(error)}` 238 | } 239 | ], 240 | isError: true 241 | }; 242 | } 243 | } 244 | ); 245 | 246 | // CreateRecordChapter tool 247 | server.tool( 248 | "obs-create-record-chapter", 249 | "Adds a new chapter marker to the file currently being recorded", 250 | { 251 | chapterName: z.string().optional().describe("Name of the new chapter") 252 | }, 253 | async ({ chapterName }) => { 254 | try { 255 | const requestParams: Record = {}; 256 | if (chapterName !== undefined) { 257 | requestParams.chapterName = chapterName; 258 | } 259 | 260 | await client.sendRequest("CreateRecordChapter", requestParams); 261 | return { 262 | content: [ 263 | { 264 | type: "text", 265 | text: `Record chapter${chapterName ? ` "${chapterName}"` : ""} created` 266 | } 267 | ] 268 | }; 269 | } catch (error) { 270 | return { 271 | content: [ 272 | { 273 | type: "text", 274 | text: `Error creating record chapter: ${error instanceof Error ? error.message : String(error)}` 275 | } 276 | ], 277 | isError: true 278 | }; 279 | } 280 | } 281 | ); 282 | } -------------------------------------------------------------------------------- /docs/protocol_split/ui.json: -------------------------------------------------------------------------------- 1 | { 2 | "requests": [ 3 | { 4 | "description": "Gets whether studio is enabled.", 5 | "requestType": "GetStudioModeEnabled", 6 | "complexity": 1, 7 | "rpcVersion": "1", 8 | "deprecated": false, 9 | "initialVersion": "5.0.0", 10 | "category": "ui", 11 | "requestFields": [], 12 | "responseFields": [ 13 | { 14 | "valueName": "studioModeEnabled", 15 | "valueType": "Boolean", 16 | "valueDescription": "Whether studio mode is enabled" 17 | } 18 | ] 19 | }, 20 | { 21 | "description": "Enables or disables studio mode", 22 | "requestType": "SetStudioModeEnabled", 23 | "complexity": 1, 24 | "rpcVersion": "1", 25 | "deprecated": false, 26 | "initialVersion": "5.0.0", 27 | "category": "ui", 28 | "requestFields": [ 29 | { 30 | "valueName": "studioModeEnabled", 31 | "valueType": "Boolean", 32 | "valueDescription": "True == Enabled, False == Disabled", 33 | "valueRestrictions": null, 34 | "valueOptional": false, 35 | "valueOptionalBehavior": null 36 | } 37 | ], 38 | "responseFields": [] 39 | }, 40 | { 41 | "description": "Opens the properties dialog of an input.", 42 | "requestType": "OpenInputPropertiesDialog", 43 | "complexity": 1, 44 | "rpcVersion": "1", 45 | "deprecated": false, 46 | "initialVersion": "5.0.0", 47 | "category": "ui", 48 | "requestFields": [ 49 | { 50 | "valueName": "inputName", 51 | "valueType": "String", 52 | "valueDescription": "Name of the input to open the dialog of", 53 | "valueRestrictions": null, 54 | "valueOptional": true, 55 | "valueOptionalBehavior": "Unknown" 56 | }, 57 | { 58 | "valueName": "inputUuid", 59 | "valueType": "String", 60 | "valueDescription": "UUID of the input to open the dialog of", 61 | "valueRestrictions": null, 62 | "valueOptional": true, 63 | "valueOptionalBehavior": "Unknown" 64 | } 65 | ], 66 | "responseFields": [] 67 | }, 68 | { 69 | "description": "Opens the filters dialog of an input.", 70 | "requestType": "OpenInputFiltersDialog", 71 | "complexity": 1, 72 | "rpcVersion": "1", 73 | "deprecated": false, 74 | "initialVersion": "5.0.0", 75 | "category": "ui", 76 | "requestFields": [ 77 | { 78 | "valueName": "inputName", 79 | "valueType": "String", 80 | "valueDescription": "Name of the input to open the dialog of", 81 | "valueRestrictions": null, 82 | "valueOptional": true, 83 | "valueOptionalBehavior": "Unknown" 84 | }, 85 | { 86 | "valueName": "inputUuid", 87 | "valueType": "String", 88 | "valueDescription": "UUID of the input to open the dialog of", 89 | "valueRestrictions": null, 90 | "valueOptional": true, 91 | "valueOptionalBehavior": "Unknown" 92 | } 93 | ], 94 | "responseFields": [] 95 | }, 96 | { 97 | "description": "Opens the interact dialog of an input.", 98 | "requestType": "OpenInputInteractDialog", 99 | "complexity": 1, 100 | "rpcVersion": "1", 101 | "deprecated": false, 102 | "initialVersion": "5.0.0", 103 | "category": "ui", 104 | "requestFields": [ 105 | { 106 | "valueName": "inputName", 107 | "valueType": "String", 108 | "valueDescription": "Name of the input to open the dialog of", 109 | "valueRestrictions": null, 110 | "valueOptional": true, 111 | "valueOptionalBehavior": "Unknown" 112 | }, 113 | { 114 | "valueName": "inputUuid", 115 | "valueType": "String", 116 | "valueDescription": "UUID of the input to open the dialog of", 117 | "valueRestrictions": null, 118 | "valueOptional": true, 119 | "valueOptionalBehavior": "Unknown" 120 | } 121 | ], 122 | "responseFields": [] 123 | }, 124 | { 125 | "description": "Gets a list of connected monitors and information about them.", 126 | "requestType": "GetMonitorList", 127 | "complexity": 2, 128 | "rpcVersion": "1", 129 | "deprecated": false, 130 | "initialVersion": "5.0.0", 131 | "category": "ui", 132 | "requestFields": [], 133 | "responseFields": [ 134 | { 135 | "valueName": "monitors", 136 | "valueType": "Array", 137 | "valueDescription": "a list of detected monitors with some information" 138 | } 139 | ] 140 | }, 141 | { 142 | "description": "Opens a projector for a specific output video mix.\n\nMix types:\n\n- `OBS_WEBSOCKET_VIDEO_MIX_TYPE_PREVIEW`\n- `OBS_WEBSOCKET_VIDEO_MIX_TYPE_PROGRAM`\n- `OBS_WEBSOCKET_VIDEO_MIX_TYPE_MULTIVIEW`\n\nNote: This request serves to provide feature parity with 4.x. It is very likely to be changed/deprecated in a future release.", 143 | "requestType": "OpenVideoMixProjector", 144 | "complexity": 3, 145 | "rpcVersion": "1", 146 | "deprecated": false, 147 | "initialVersion": "5.0.0", 148 | "category": "ui", 149 | "requestFields": [ 150 | { 151 | "valueName": "videoMixType", 152 | "valueType": "String", 153 | "valueDescription": "Type of mix to open", 154 | "valueRestrictions": null, 155 | "valueOptional": false, 156 | "valueOptionalBehavior": null 157 | }, 158 | { 159 | "valueName": "monitorIndex", 160 | "valueType": "Number", 161 | "valueDescription": "Monitor index, use `GetMonitorList` to obtain index", 162 | "valueRestrictions": null, 163 | "valueOptional": true, 164 | "valueOptionalBehavior": "-1: Opens projector in windowed mode" 165 | }, 166 | { 167 | "valueName": "projectorGeometry", 168 | "valueType": "String", 169 | "valueDescription": "Size/Position data for a windowed projector, in Qt Base64 encoded format. Mutually exclusive with `monitorIndex`", 170 | "valueRestrictions": null, 171 | "valueOptional": true, 172 | "valueOptionalBehavior": "N/A" 173 | } 174 | ], 175 | "responseFields": [] 176 | }, 177 | { 178 | "description": "Opens a projector for a source.\n\nNote: This request serves to provide feature parity with 4.x. It is very likely to be changed/deprecated in a future release.", 179 | "requestType": "OpenSourceProjector", 180 | "complexity": 3, 181 | "rpcVersion": "1", 182 | "deprecated": false, 183 | "initialVersion": "5.0.0", 184 | "category": "ui", 185 | "requestFields": [ 186 | { 187 | "valueName": "sourceName", 188 | "valueType": "String", 189 | "valueDescription": "Name of the source to open a projector for", 190 | "valueRestrictions": null, 191 | "valueOptional": true, 192 | "valueOptionalBehavior": "Unknown" 193 | }, 194 | { 195 | "valueName": "sourceUuid", 196 | "valueType": "String", 197 | "valueDescription": "UUID of the source to open a projector for", 198 | "valueRestrictions": null, 199 | "valueOptional": true, 200 | "valueOptionalBehavior": "Unknown" 201 | }, 202 | { 203 | "valueName": "monitorIndex", 204 | "valueType": "Number", 205 | "valueDescription": "Monitor index, use `GetMonitorList` to obtain index", 206 | "valueRestrictions": null, 207 | "valueOptional": true, 208 | "valueOptionalBehavior": "-1: Opens projector in windowed mode" 209 | }, 210 | { 211 | "valueName": "projectorGeometry", 212 | "valueType": "String", 213 | "valueDescription": "Size/Position data for a windowed projector, in Qt Base64 encoded format. Mutually exclusive with `monitorIndex`", 214 | "valueRestrictions": null, 215 | "valueOptional": true, 216 | "valueOptionalBehavior": "N/A" 217 | } 218 | ], 219 | "responseFields": [] 220 | } 221 | ] 222 | } -------------------------------------------------------------------------------- /docs/protocol_split/transitions.json: -------------------------------------------------------------------------------- 1 | { 2 | "requests": [ 3 | { 4 | "description": "Gets an array of all available transition kinds.\n\nSimilar to `GetInputKindList`", 5 | "requestType": "GetTransitionKindList", 6 | "complexity": 2, 7 | "rpcVersion": "1", 8 | "deprecated": false, 9 | "initialVersion": "5.0.0", 10 | "category": "transitions", 11 | "requestFields": [], 12 | "responseFields": [ 13 | { 14 | "valueName": "transitionKinds", 15 | "valueType": "Array", 16 | "valueDescription": "Array of transition kinds" 17 | } 18 | ] 19 | }, 20 | { 21 | "description": "Gets an array of all scene transitions in OBS.", 22 | "requestType": "GetSceneTransitionList", 23 | "complexity": 3, 24 | "rpcVersion": "1", 25 | "deprecated": false, 26 | "initialVersion": "5.0.0", 27 | "category": "transitions", 28 | "requestFields": [], 29 | "responseFields": [ 30 | { 31 | "valueName": "currentSceneTransitionName", 32 | "valueType": "String", 33 | "valueDescription": "Name of the current scene transition. Can be null" 34 | }, 35 | { 36 | "valueName": "currentSceneTransitionUuid", 37 | "valueType": "String", 38 | "valueDescription": "UUID of the current scene transition. Can be null" 39 | }, 40 | { 41 | "valueName": "currentSceneTransitionKind", 42 | "valueType": "String", 43 | "valueDescription": "Kind of the current scene transition. Can be null" 44 | }, 45 | { 46 | "valueName": "transitions", 47 | "valueType": "Array", 48 | "valueDescription": "Array of transitions" 49 | } 50 | ] 51 | }, 52 | { 53 | "description": "Gets information about the current scene transition.", 54 | "requestType": "GetCurrentSceneTransition", 55 | "complexity": 2, 56 | "rpcVersion": "1", 57 | "deprecated": false, 58 | "initialVersion": "5.0.0", 59 | "category": "transitions", 60 | "requestFields": [], 61 | "responseFields": [ 62 | { 63 | "valueName": "transitionName", 64 | "valueType": "String", 65 | "valueDescription": "Name of the transition" 66 | }, 67 | { 68 | "valueName": "transitionUuid", 69 | "valueType": "String", 70 | "valueDescription": "UUID of the transition" 71 | }, 72 | { 73 | "valueName": "transitionKind", 74 | "valueType": "String", 75 | "valueDescription": "Kind of the transition" 76 | }, 77 | { 78 | "valueName": "transitionFixed", 79 | "valueType": "Boolean", 80 | "valueDescription": "Whether the transition uses a fixed (unconfigurable) duration" 81 | }, 82 | { 83 | "valueName": "transitionDuration", 84 | "valueType": "Number", 85 | "valueDescription": "Configured transition duration in milliseconds. `null` if transition is fixed" 86 | }, 87 | { 88 | "valueName": "transitionConfigurable", 89 | "valueType": "Boolean", 90 | "valueDescription": "Whether the transition supports being configured" 91 | }, 92 | { 93 | "valueName": "transitionSettings", 94 | "valueType": "Object", 95 | "valueDescription": "Object of settings for the transition. `null` if transition is not configurable" 96 | } 97 | ] 98 | }, 99 | { 100 | "description": "Sets the current scene transition.\n\nSmall note: While the namespace of scene transitions is generally unique, that uniqueness is not a guarantee as it is with other resources like inputs.", 101 | "requestType": "SetCurrentSceneTransition", 102 | "complexity": 2, 103 | "rpcVersion": "1", 104 | "deprecated": false, 105 | "initialVersion": "5.0.0", 106 | "category": "transitions", 107 | "requestFields": [ 108 | { 109 | "valueName": "transitionName", 110 | "valueType": "String", 111 | "valueDescription": "Name of the transition to make active", 112 | "valueRestrictions": null, 113 | "valueOptional": false, 114 | "valueOptionalBehavior": null 115 | } 116 | ], 117 | "responseFields": [] 118 | }, 119 | { 120 | "description": "Sets the duration of the current scene transition, if it is not fixed.", 121 | "requestType": "SetCurrentSceneTransitionDuration", 122 | "complexity": 2, 123 | "rpcVersion": "1", 124 | "deprecated": false, 125 | "initialVersion": "5.0.0", 126 | "category": "transitions", 127 | "requestFields": [ 128 | { 129 | "valueName": "transitionDuration", 130 | "valueType": "Number", 131 | "valueDescription": "Duration in milliseconds", 132 | "valueRestrictions": ">= 50, <= 20000", 133 | "valueOptional": false, 134 | "valueOptionalBehavior": null 135 | } 136 | ], 137 | "responseFields": [] 138 | }, 139 | { 140 | "description": "Sets the settings of the current scene transition.", 141 | "requestType": "SetCurrentSceneTransitionSettings", 142 | "complexity": 3, 143 | "rpcVersion": "1", 144 | "deprecated": false, 145 | "initialVersion": "5.0.0", 146 | "category": "transitions", 147 | "requestFields": [ 148 | { 149 | "valueName": "transitionSettings", 150 | "valueType": "Object", 151 | "valueDescription": "Settings object to apply to the transition. Can be `{}`", 152 | "valueRestrictions": null, 153 | "valueOptional": false, 154 | "valueOptionalBehavior": null 155 | }, 156 | { 157 | "valueName": "overlay", 158 | "valueType": "Boolean", 159 | "valueDescription": "Whether to overlay over the current settings or replace them", 160 | "valueRestrictions": null, 161 | "valueOptional": true, 162 | "valueOptionalBehavior": "true" 163 | } 164 | ], 165 | "responseFields": [] 166 | }, 167 | { 168 | "description": "Gets the cursor position of the current scene transition.\n\nNote: `transitionCursor` will return 1.0 when the transition is inactive.", 169 | "requestType": "GetCurrentSceneTransitionCursor", 170 | "complexity": 2, 171 | "rpcVersion": "1", 172 | "deprecated": false, 173 | "initialVersion": "5.0.0", 174 | "category": "transitions", 175 | "requestFields": [], 176 | "responseFields": [ 177 | { 178 | "valueName": "transitionCursor", 179 | "valueType": "Number", 180 | "valueDescription": "Cursor position, between 0.0 and 1.0" 181 | } 182 | ] 183 | }, 184 | { 185 | "description": "Triggers the current scene transition. Same functionality as the `Transition` button in studio mode.", 186 | "requestType": "TriggerStudioModeTransition", 187 | "complexity": 1, 188 | "rpcVersion": "1", 189 | "deprecated": false, 190 | "initialVersion": "5.0.0", 191 | "category": "transitions", 192 | "requestFields": [], 193 | "responseFields": [] 194 | }, 195 | { 196 | "description": "Sets the position of the TBar.\n\n**Very important note**: This will be deprecated and replaced in a future version of obs-websocket.", 197 | "requestType": "SetTBarPosition", 198 | "complexity": 3, 199 | "rpcVersion": "1", 200 | "deprecated": false, 201 | "initialVersion": "5.0.0", 202 | "category": "transitions", 203 | "requestFields": [ 204 | { 205 | "valueName": "position", 206 | "valueType": "Number", 207 | "valueDescription": "New position", 208 | "valueRestrictions": ">= 0.0, <= 1.0", 209 | "valueOptional": false, 210 | "valueOptionalBehavior": null 211 | }, 212 | { 213 | "valueName": "release", 214 | "valueType": "Boolean", 215 | "valueDescription": "Whether to release the TBar. Only set `false` if you know that you will be sending another position update", 216 | "valueRestrictions": null, 217 | "valueOptional": true, 218 | "valueOptionalBehavior": "`true`" 219 | } 220 | ], 221 | "responseFields": [] 222 | } 223 | ] 224 | } -------------------------------------------------------------------------------- /src/tools/transitions.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { OBSWebSocketClient } from "../client.js"; 3 | import { z } from "zod"; 4 | 5 | export async function initialize(server: McpServer, client: OBSWebSocketClient): Promise { 6 | // GetTransitionList tool 7 | server.tool( 8 | "obs-get-transition-list", 9 | "Get a list of available transitions in OBS", 10 | {}, 11 | async () => { 12 | try { 13 | const transitions = await client.sendRequest("GetTransitionList"); 14 | return { 15 | content: [ 16 | { 17 | type: "text", 18 | text: JSON.stringify(transitions, null, 2) 19 | } 20 | ] 21 | }; 22 | } catch (error) { 23 | return { 24 | content: [ 25 | { 26 | type: "text", 27 | text: `Error getting transition list: ${error instanceof Error ? error.message : String(error)}` 28 | } 29 | ], 30 | isError: true 31 | }; 32 | } 33 | } 34 | ); 35 | 36 | // GetCurrentTransition tool 37 | server.tool( 38 | "obs-get-current-transition", 39 | "Get the name of the currently active transition", 40 | {}, 41 | async () => { 42 | try { 43 | const transition = await client.sendRequest("GetCurrentSceneTransition"); 44 | return { 45 | content: [ 46 | { 47 | type: "text", 48 | text: `Current transition: ${transition.transitionName} (${transition.transitionKind}) with duration: ${transition.transitionDuration}ms` 49 | } 50 | ] 51 | }; 52 | } catch (error) { 53 | return { 54 | content: [ 55 | { 56 | type: "text", 57 | text: `Error getting current transition: ${error instanceof Error ? error.message : String(error)}` 58 | } 59 | ], 60 | isError: true 61 | }; 62 | } 63 | } 64 | ); 65 | 66 | // SetCurrentTransition tool 67 | server.tool( 68 | "obs-set-current-transition", 69 | "Set the current transition in OBS", 70 | { 71 | transitionName: z.string().describe("The name of the transition to set as current") 72 | }, 73 | async ({ transitionName }) => { 74 | try { 75 | await client.sendRequest("SetCurrentSceneTransition", { transitionName }); 76 | return { 77 | content: [ 78 | { 79 | type: "text", 80 | text: `Successfully set current transition to: ${transitionName}` 81 | } 82 | ] 83 | }; 84 | } catch (error) { 85 | return { 86 | content: [ 87 | { 88 | type: "text", 89 | text: `Error setting current transition: ${error instanceof Error ? error.message : String(error)}` 90 | } 91 | ], 92 | isError: true 93 | }; 94 | } 95 | } 96 | ); 97 | 98 | // GetTransitionDuration tool 99 | server.tool( 100 | "obs-get-transition-duration", 101 | "Get the duration of the current transition in milliseconds", 102 | {}, 103 | async () => { 104 | try { 105 | const duration = await client.sendRequest("GetCurrentSceneTransitionDuration"); 106 | return { 107 | content: [ 108 | { 109 | type: "text", 110 | text: `Current transition duration: ${duration.transitionDuration}ms` 111 | } 112 | ] 113 | }; 114 | } catch (error) { 115 | return { 116 | content: [ 117 | { 118 | type: "text", 119 | text: `Error getting transition duration: ${error instanceof Error ? error.message : String(error)}` 120 | } 121 | ], 122 | isError: true 123 | }; 124 | } 125 | } 126 | ); 127 | 128 | // SetTransitionDuration tool 129 | server.tool( 130 | "obs-set-transition-duration", 131 | "Set the duration of the current transition in milliseconds", 132 | { 133 | duration: z.number().min(0).describe("The duration to set in milliseconds") 134 | }, 135 | async ({ duration }) => { 136 | try { 137 | await client.sendRequest("SetCurrentSceneTransitionDuration", { transitionDuration: duration }); 138 | return { 139 | content: [ 140 | { 141 | type: "text", 142 | text: `Successfully set transition duration to: ${duration}ms` 143 | } 144 | ] 145 | }; 146 | } catch (error) { 147 | return { 148 | content: [ 149 | { 150 | type: "text", 151 | text: `Error setting transition duration: ${error instanceof Error ? error.message : String(error)}` 152 | } 153 | ], 154 | isError: true 155 | }; 156 | } 157 | } 158 | ); 159 | 160 | // GetTransitionKind tool 161 | server.tool( 162 | "obs-get-transition-kind", 163 | "Get the kind/type of the current transition", 164 | {}, 165 | async () => { 166 | try { 167 | const transition = await client.sendRequest("GetCurrentSceneTransition"); 168 | return { 169 | content: [ 170 | { 171 | type: "text", 172 | text: `Current transition kind: ${transition.transitionKind}` 173 | } 174 | ] 175 | }; 176 | } catch (error) { 177 | return { 178 | content: [ 179 | { 180 | type: "text", 181 | text: `Error getting transition kind: ${error instanceof Error ? error.message : String(error)}` 182 | } 183 | ], 184 | isError: true 185 | }; 186 | } 187 | } 188 | ); 189 | 190 | // SetTransitionSettings tool 191 | server.tool( 192 | "obs-set-transition-settings", 193 | "Set the settings of the current transition", 194 | { 195 | transitionSettings: z.record(z.any()).describe("The settings to apply to the transition") 196 | }, 197 | async ({ transitionSettings }) => { 198 | try { 199 | await client.sendRequest("SetCurrentSceneTransitionSettings", { transitionSettings }); 200 | return { 201 | content: [ 202 | { 203 | type: "text", 204 | text: "Successfully updated current transition settings" 205 | } 206 | ] 207 | }; 208 | } catch (error) { 209 | return { 210 | content: [ 211 | { 212 | type: "text", 213 | text: `Error setting transition settings: ${error instanceof Error ? error.message : String(error)}` 214 | } 215 | ], 216 | isError: true 217 | }; 218 | } 219 | } 220 | ); 221 | 222 | // GetTransitionSettings tool 223 | server.tool( 224 | "obs-get-transition-settings", 225 | "Get the settings of the current transition", 226 | {}, 227 | async () => { 228 | try { 229 | const settings = await client.sendRequest("GetCurrentSceneTransitionSettings"); 230 | return { 231 | content: [ 232 | { 233 | type: "text", 234 | text: JSON.stringify(settings, null, 2) 235 | } 236 | ] 237 | }; 238 | } catch (error) { 239 | return { 240 | content: [ 241 | { 242 | type: "text", 243 | text: `Error getting transition settings: ${error instanceof Error ? error.message : String(error)}` 244 | } 245 | ], 246 | isError: true 247 | }; 248 | } 249 | } 250 | ); 251 | 252 | // TriggerStudioModeTransition tool 253 | server.tool( 254 | "obs-trigger-transition", 255 | "Trigger a scene transition in OBS (Studio Mode must be enabled)", 256 | {}, 257 | async () => { 258 | try { 259 | await client.sendRequest("TriggerStudioModeTransition"); 260 | return { 261 | content: [ 262 | { 263 | type: "text", 264 | text: "Successfully triggered studio mode transition" 265 | } 266 | ] 267 | }; 268 | } catch (error) { 269 | return { 270 | content: [ 271 | { 272 | type: "text", 273 | text: `Error triggering transition: ${error instanceof Error ? error.message : String(error)}` 274 | } 275 | ], 276 | isError: true 277 | }; 278 | } 279 | } 280 | ); 281 | } -------------------------------------------------------------------------------- /py_src/scene_items.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from typing import Any, Dict, List, Optional, Union 4 | 5 | from .client import obs_client 6 | from .server import mcp 7 | 8 | @mcp.tool() 9 | async def get_scene_item_list(scene_name: str) -> List[Dict[str, Any]]: 10 | """ 11 | Gets a list of all scene items in a scene. 12 | 13 | Args: 14 | scene_name: Name of the scene to get the items of 15 | 16 | Returns: 17 | List of scene items (each with sceneItemId, sourceName, sourceKind, sceneItemIndex) 18 | """ 19 | response = await obs_client.send_request("GetSceneItemList", {"sceneName": scene_name}) 20 | return response.get("sceneItems", []) 21 | 22 | @mcp.tool() 23 | async def get_group_item_list(scene_name: str, group_name: str) -> List[Dict[str, Any]]: 24 | """ 25 | Gets a list of all scene items in a group. 26 | 27 | Args: 28 | scene_name: Name of the scene the group is in 29 | group_name: Name of the group to get the items of 30 | 31 | Returns: 32 | List of scene items (each with sceneItemId, sourceName, sourceKind, sceneItemIndex) 33 | """ 34 | response = await obs_client.send_request("GetGroupSceneItemList", { 35 | "sceneName": scene_name, 36 | "groupName": group_name 37 | }) 38 | return response.get("sceneItems", []) 39 | 40 | @mcp.tool() 41 | async def create_scene_item(scene_name: str, source_name: str, enabled: bool = True) -> int: 42 | """ 43 | Creates a new scene item in a scene. 44 | 45 | Args: 46 | scene_name: Name of the scene to create the item in 47 | source_name: Name of the source to add to the scene 48 | enabled: Whether to set the scene item to enabled or disabled 49 | 50 | Returns: 51 | ID of the created scene item 52 | """ 53 | response = await obs_client.send_request("CreateSceneItem", { 54 | "sceneName": scene_name, 55 | "sourceName": source_name, 56 | "sceneItemEnabled": enabled 57 | }) 58 | return response.get("sceneItemId", 0) 59 | 60 | @mcp.tool() 61 | async def remove_scene_item(scene_name: str, scene_item_id: int) -> None: 62 | """ 63 | Removes a scene item from a scene. 64 | 65 | Args: 66 | scene_name: Name of the scene the item is in 67 | scene_item_id: ID of the scene item to remove 68 | """ 69 | await obs_client.send_request("RemoveSceneItem", { 70 | "sceneName": scene_name, 71 | "sceneItemId": scene_item_id 72 | }) 73 | 74 | @mcp.tool() 75 | async def duplicate_scene_item(scene_name: str, scene_item_id: int, 76 | destination_scene_name: Optional[str] = None) -> int: 77 | """ 78 | Duplicates a scene item in a scene. 79 | 80 | Args: 81 | scene_name: Name of the scene the item is in 82 | scene_item_id: ID of the scene item to duplicate 83 | destination_scene_name: Name of the scene to create the duplicated item in (defaults to original scene) 84 | 85 | Returns: 86 | ID of the duplicated scene item 87 | """ 88 | payload = { 89 | "sceneName": scene_name, 90 | "sceneItemId": scene_item_id 91 | } 92 | 93 | if destination_scene_name: 94 | payload["destinationSceneName"] = destination_scene_name 95 | 96 | response = await obs_client.send_request("DuplicateSceneItem", payload) 97 | return response.get("sceneItemId", 0) 98 | 99 | @mcp.tool() 100 | async def get_scene_item_id(scene_name: str, source_name: str, search_offset: int = 0) -> int: 101 | """ 102 | Gets the ID of a scene item in a scene. 103 | 104 | Args: 105 | scene_name: Name of the scene the item is in 106 | source_name: Name of the source to find the ID of 107 | search_offset: Number of matches to skip 108 | 109 | Returns: 110 | ID of the scene item 111 | """ 112 | response = await obs_client.send_request("GetSceneItemId", { 113 | "sceneName": scene_name, 114 | "sourceName": source_name, 115 | "searchOffset": search_offset 116 | }) 117 | return response.get("sceneItemId", 0) 118 | 119 | @mcp.tool() 120 | async def get_scene_item_enabled(scene_name: str, scene_item_id: int) -> bool: 121 | """ 122 | Gets the enabled state of a scene item. 123 | 124 | Args: 125 | scene_name: Name of the scene the item is in 126 | scene_item_id: ID of the scene item 127 | 128 | Returns: 129 | Whether the scene item is enabled 130 | """ 131 | response = await obs_client.send_request("GetSceneItemEnabled", { 132 | "sceneName": scene_name, 133 | "sceneItemId": scene_item_id 134 | }) 135 | return response.get("sceneItemEnabled", False) 136 | 137 | @mcp.tool() 138 | async def set_scene_item_enabled(scene_name: str, scene_item_id: int, enabled: bool) -> None: 139 | """ 140 | Sets the enabled state of a scene item. 141 | 142 | Args: 143 | scene_name: Name of the scene the item is in 144 | scene_item_id: ID of the scene item 145 | enabled: New enabled state of the scene item 146 | """ 147 | await obs_client.send_request("SetSceneItemEnabled", { 148 | "sceneName": scene_name, 149 | "sceneItemId": scene_item_id, 150 | "sceneItemEnabled": enabled 151 | }) 152 | 153 | @mcp.tool() 154 | async def get_scene_item_locked(scene_name: str, scene_item_id: int) -> bool: 155 | """ 156 | Gets the locked state of a scene item. 157 | 158 | Args: 159 | scene_name: Name of the scene the item is in 160 | scene_item_id: ID of the scene item 161 | 162 | Returns: 163 | Whether the scene item is locked 164 | """ 165 | response = await obs_client.send_request("GetSceneItemLocked", { 166 | "sceneName": scene_name, 167 | "sceneItemId": scene_item_id 168 | }) 169 | return response.get("sceneItemLocked", False) 170 | 171 | @mcp.tool() 172 | async def set_scene_item_locked(scene_name: str, scene_item_id: int, locked: bool) -> None: 173 | """ 174 | Sets the locked state of a scene item. 175 | 176 | Args: 177 | scene_name: Name of the scene the item is in 178 | scene_item_id: ID of the scene item 179 | locked: New locked state of the scene item 180 | """ 181 | await obs_client.send_request("SetSceneItemLocked", { 182 | "sceneName": scene_name, 183 | "sceneItemId": scene_item_id, 184 | "sceneItemLocked": locked 185 | }) 186 | 187 | @mcp.tool() 188 | async def get_scene_item_index(scene_name: str, scene_item_id: int) -> int: 189 | """ 190 | Gets the index position of a scene item in a scene. 191 | 192 | Args: 193 | scene_name: Name of the scene the item is in 194 | scene_item_id: ID of the scene item 195 | 196 | Returns: 197 | Index position of the scene item 198 | """ 199 | response = await obs_client.send_request("GetSceneItemIndex", { 200 | "sceneName": scene_name, 201 | "sceneItemId": scene_item_id 202 | }) 203 | return response.get("sceneItemIndex", 0) 204 | 205 | @mcp.tool() 206 | async def set_scene_item_index(scene_name: str, scene_item_id: int, index: int) -> None: 207 | """ 208 | Sets the index position of a scene item in a scene. 209 | 210 | Args: 211 | scene_name: Name of the scene the item is in 212 | scene_item_id: ID of the scene item 213 | index: New index position for the scene item 214 | """ 215 | await obs_client.send_request("SetSceneItemIndex", { 216 | "sceneName": scene_name, 217 | "sceneItemId": scene_item_id, 218 | "sceneItemIndex": index 219 | }) 220 | 221 | @mcp.tool() 222 | async def get_scene_item_transform(scene_name: str, scene_item_id: int) -> Dict[str, Any]: 223 | """ 224 | Gets the transform/crop info of a scene item. 225 | 226 | Args: 227 | scene_name: Name of the scene the item is in 228 | scene_item_id: ID of the scene item 229 | 230 | Returns: 231 | Dict with transform information (position, rotation, scale, crop, bounds) 232 | """ 233 | return await obs_client.send_request("GetSceneItemTransform", { 234 | "sceneName": scene_name, 235 | "sceneItemId": scene_item_id 236 | }) 237 | 238 | @mcp.tool() 239 | async def set_scene_item_transform(scene_name: str, scene_item_id: int, transform: Dict[str, Any]) -> None: 240 | """ 241 | Sets the transform/crop info of a scene item. 242 | 243 | Args: 244 | scene_name: Name of the scene the item is in 245 | scene_item_id: ID of the scene item 246 | transform: Dict with transform properties to set 247 | """ 248 | await obs_client.send_request("SetSceneItemTransform", { 249 | "sceneName": scene_name, 250 | "sceneItemId": scene_item_id, 251 | "sceneItemTransform": transform 252 | }) 253 | 254 | @mcp.tool() 255 | async def get_scene_item_blend_mode(scene_name: str, scene_item_id: int) -> str: 256 | """ 257 | Gets the blend mode of a scene item. 258 | 259 | Args: 260 | scene_name: Name of the scene the item is in 261 | scene_item_id: ID of the scene item 262 | 263 | Returns: 264 | Current blend mode of the scene item 265 | """ 266 | response = await obs_client.send_request("GetSceneItemBlendMode", { 267 | "sceneName": scene_name, 268 | "sceneItemId": scene_item_id 269 | }) 270 | return response.get("sceneItemBlendMode", "") 271 | 272 | @mcp.tool() 273 | async def set_scene_item_blend_mode(scene_name: str, scene_item_id: int, blend_mode: str) -> None: 274 | """ 275 | Sets the blend mode of a scene item. 276 | 277 | Args: 278 | scene_name: Name of the scene the item is in 279 | scene_item_id: ID of the scene item 280 | blend_mode: New blend mode (OBS_BLEND_NORMAL, etc.) 281 | """ 282 | await obs_client.send_request("SetSceneItemBlendMode", { 283 | "sceneName": scene_name, 284 | "sceneItemId": scene_item_id, 285 | "sceneItemBlendMode": blend_mode 286 | }) -------------------------------------------------------------------------------- /src/tools/scene-items.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { OBSWebSocketClient } from "../client.js"; 3 | import { z } from "zod"; 4 | 5 | export async function initialize(server: McpServer, client: OBSWebSocketClient): Promise { 6 | // GetSceneItemList tool 7 | server.tool( 8 | "obs-get-scene-items", 9 | "Get a list of all scene items in a scene", 10 | { 11 | sceneName: z.string().describe("The name of the scene to get items from") 12 | }, 13 | async ({ sceneName }) => { 14 | try { 15 | const sceneItems = await client.sendRequest("GetSceneItemList", { sceneName }); 16 | return { 17 | content: [ 18 | { 19 | type: "text", 20 | text: JSON.stringify(sceneItems, null, 2) 21 | } 22 | ] 23 | }; 24 | } catch (error) { 25 | return { 26 | content: [ 27 | { 28 | type: "text", 29 | text: `Error getting scene items: ${error instanceof Error ? error.message : String(error)}` 30 | } 31 | ], 32 | isError: true 33 | }; 34 | } 35 | } 36 | ); 37 | 38 | // CreateSceneItem tool 39 | server.tool( 40 | "obs-create-scene-item", 41 | "Create a scene item for a source in a scene", 42 | { 43 | sceneName: z.string().describe("The scene to add the source to"), 44 | sourceName: z.string().describe("The name of the source to add"), 45 | enabled: z.boolean().optional().describe("Whether the scene item is enabled/visible (default: true)") 46 | }, 47 | async ({ sceneName, sourceName, enabled = true }) => { 48 | try { 49 | const response = await client.sendRequest("CreateSceneItem", { 50 | sceneName, 51 | sourceName, 52 | sceneItemEnabled: enabled 53 | }); 54 | 55 | return { 56 | content: [ 57 | { 58 | type: "text", 59 | text: `Successfully added ${sourceName} to ${sceneName} with ID: ${response.sceneItemId}` 60 | } 61 | ] 62 | }; 63 | } catch (error) { 64 | return { 65 | content: [ 66 | { 67 | type: "text", 68 | text: `Error creating scene item: ${error instanceof Error ? error.message : String(error)}` 69 | } 70 | ], 71 | isError: true 72 | }; 73 | } 74 | } 75 | ); 76 | 77 | // RemoveSceneItem tool 78 | server.tool( 79 | "obs-remove-scene-item", 80 | "Remove a scene item from a scene", 81 | { 82 | sceneName: z.string().describe("The scene to remove the item from"), 83 | sceneItemId: z.number().describe("The ID of the scene item to remove") 84 | }, 85 | async ({ sceneName, sceneItemId }) => { 86 | try { 87 | await client.sendRequest("RemoveSceneItem", { sceneName, sceneItemId }); 88 | 89 | return { 90 | content: [ 91 | { 92 | type: "text", 93 | text: `Successfully removed item with ID ${sceneItemId} from ${sceneName}` 94 | } 95 | ] 96 | }; 97 | } catch (error) { 98 | return { 99 | content: [ 100 | { 101 | type: "text", 102 | text: `Error removing scene item: ${error instanceof Error ? error.message : String(error)}` 103 | } 104 | ], 105 | isError: true 106 | }; 107 | } 108 | } 109 | ); 110 | 111 | // SetSceneItemEnabled tool 112 | server.tool( 113 | "obs-set-scene-item-enabled", 114 | "Show or hide a scene item", 115 | { 116 | sceneName: z.string().describe("The scene that the source belongs to"), 117 | sceneItemId: z.number().describe("The ID of the scene item"), 118 | enabled: z.boolean().describe("Whether to show (true) or hide (false) the item") 119 | }, 120 | async ({ sceneName, sceneItemId, enabled }) => { 121 | try { 122 | await client.sendRequest("SetSceneItemEnabled", { 123 | sceneName, 124 | sceneItemId, 125 | sceneItemEnabled: enabled 126 | }); 127 | 128 | return { 129 | content: [ 130 | { 131 | type: "text", 132 | text: `Successfully ${enabled ? "showed" : "hid"} item with ID ${sceneItemId} in ${sceneName}` 133 | } 134 | ] 135 | }; 136 | } catch (error) { 137 | return { 138 | content: [ 139 | { 140 | type: "text", 141 | text: `Error setting scene item visibility: ${error instanceof Error ? error.message : String(error)}` 142 | } 143 | ], 144 | isError: true 145 | }; 146 | } 147 | } 148 | ); 149 | 150 | // GetSceneItemTransform tool 151 | server.tool( 152 | "obs-get-scene-item-transform", 153 | "Get the position, rotation, scale, or crop of a scene item", 154 | { 155 | sceneName: z.string().describe("The scene the item is in"), 156 | sceneItemId: z.number().describe("The ID of the scene item") 157 | }, 158 | async ({ sceneName, sceneItemId }) => { 159 | try { 160 | const response = await client.sendRequest("GetSceneItemTransform", { sceneName, sceneItemId }); 161 | return { 162 | content: [ 163 | { 164 | type: "text", 165 | text: JSON.stringify(response, null, 2) 166 | } 167 | ] 168 | }; 169 | } catch (error) { 170 | return { 171 | content: [ 172 | { 173 | type: "text", 174 | text: `Error getting scene item transform: ${error instanceof Error ? error.message : String(error)}` 175 | } 176 | ], 177 | isError: true 178 | }; 179 | } 180 | } 181 | ); 182 | 183 | // SetSceneItemTransform tool 184 | server.tool( 185 | "obs-set-scene-item-transform", 186 | "Set the position, rotation, scale, or crop of a scene item", 187 | { 188 | sceneName: z.string().describe("The scene the item is in"), 189 | sceneItemId: z.number().describe("The ID of the scene item"), 190 | positionX: z.number().optional().describe("The x position"), 191 | positionY: z.number().optional().describe("The y position"), 192 | rotation: z.number().optional().describe("The rotation in degrees"), 193 | scaleX: z.number().optional().describe("The x scale factor"), 194 | scaleY: z.number().optional().describe("The y scale factor"), 195 | cropTop: z.number().optional().describe("The number of pixels cropped off the top"), 196 | cropBottom: z.number().optional().describe("The number of pixels cropped off the bottom"), 197 | cropLeft: z.number().optional().describe("The number of pixels cropped off the left"), 198 | cropRight: z.number().optional().describe("The number of pixels cropped off the right") 199 | }, 200 | async (params) => { 201 | try { 202 | const { sceneName, sceneItemId, ...transformParams } = params; 203 | 204 | // Build the transform object 205 | const sceneItemTransform: Record = {}; 206 | 207 | if (transformParams.positionX !== undefined || transformParams.positionY !== undefined) { 208 | sceneItemTransform.positionX = transformParams.positionX; 209 | sceneItemTransform.positionY = transformParams.positionY; 210 | } 211 | 212 | if (transformParams.rotation !== undefined) { 213 | sceneItemTransform.rotation = transformParams.rotation; 214 | } 215 | 216 | if (transformParams.scaleX !== undefined || transformParams.scaleY !== undefined) { 217 | sceneItemTransform.scaleX = transformParams.scaleX; 218 | sceneItemTransform.scaleY = transformParams.scaleY; 219 | } 220 | 221 | if (transformParams.cropTop !== undefined || transformParams.cropBottom !== undefined || 222 | transformParams.cropLeft !== undefined || transformParams.cropRight !== undefined) { 223 | if (transformParams.cropTop !== undefined) sceneItemTransform.cropTop = transformParams.cropTop; 224 | if (transformParams.cropBottom !== undefined) sceneItemTransform.cropBottom = transformParams.cropBottom; 225 | if (transformParams.cropLeft !== undefined) sceneItemTransform.cropLeft = transformParams.cropLeft; 226 | if (transformParams.cropRight !== undefined) sceneItemTransform.cropRight = transformParams.cropRight; 227 | } 228 | 229 | await client.sendRequest("SetSceneItemTransform", { 230 | sceneName, 231 | sceneItemId, 232 | sceneItemTransform 233 | }); 234 | 235 | return { 236 | content: [ 237 | { 238 | type: "text", 239 | text: `Successfully updated transform for item with ID ${sceneItemId} in ${sceneName}` 240 | } 241 | ] 242 | }; 243 | } catch (error) { 244 | return { 245 | content: [ 246 | { 247 | type: "text", 248 | text: `Error setting scene item transform: ${error instanceof Error ? error.message : String(error)}` 249 | } 250 | ], 251 | isError: true 252 | }; 253 | } 254 | } 255 | ); 256 | 257 | // GetSceneItemIdByName tool 258 | server.tool( 259 | "obs-get-scene-item-id", 260 | "Get the ID of a scene item by its source name", 261 | { 262 | sceneName: z.string().describe("The scene name to search in"), 263 | sourceName: z.string().describe("The source name to find") 264 | }, 265 | async ({ sceneName, sourceName }) => { 266 | try { 267 | const response = await client.sendRequest("GetSceneItemId", { 268 | sceneName, 269 | sourceName 270 | }); 271 | 272 | return { 273 | content: [ 274 | { 275 | type: "text", 276 | text: `Scene item ID for ${sourceName} in ${sceneName}: ${response.sceneItemId}` 277 | } 278 | ] 279 | }; 280 | } catch (error) { 281 | return { 282 | content: [ 283 | { 284 | type: "text", 285 | text: `Error getting scene item ID: ${error instanceof Error ? error.message : String(error)}` 286 | } 287 | ], 288 | isError: true 289 | }; 290 | } 291 | } 292 | ); 293 | } -------------------------------------------------------------------------------- /src/tools/ui.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { OBSWebSocketClient } from "../client.js"; 3 | import { z } from "zod"; 4 | 5 | export async function initialize(server: McpServer, client: OBSWebSocketClient): Promise { 6 | // GetStudioModeEnabled tool 7 | server.tool( 8 | "obs-get-studio-mode", 9 | "Gets whether studio mode is enabled", 10 | {}, 11 | async () => { 12 | try { 13 | const response = await client.sendRequest("GetStudioModeEnabled"); 14 | return { 15 | content: [ 16 | { 17 | type: "text", 18 | text: `Studio Mode is ${response.studioModeEnabled ? "enabled" : "disabled"}` 19 | } 20 | ] 21 | }; 22 | } catch (error) { 23 | return { 24 | content: [ 25 | { 26 | type: "text", 27 | text: `Error checking studio mode: ${error instanceof Error ? error.message : String(error)}` 28 | } 29 | ], 30 | isError: true 31 | }; 32 | } 33 | } 34 | ); 35 | 36 | // SetStudioModeEnabled tool 37 | server.tool( 38 | "obs-set-studio-mode", 39 | "Enables or disables studio mode", 40 | { 41 | studioModeEnabled: z.boolean().describe("Whether to enable (true) or disable (false) Studio Mode") 42 | }, 43 | async ({ studioModeEnabled }) => { 44 | try { 45 | await client.sendRequest("SetStudioModeEnabled", { studioModeEnabled }); 46 | return { 47 | content: [ 48 | { 49 | type: "text", 50 | text: `Studio Mode has been ${studioModeEnabled ? "enabled" : "disabled"}` 51 | } 52 | ] 53 | }; 54 | } catch (error) { 55 | return { 56 | content: [ 57 | { 58 | type: "text", 59 | text: `Error setting studio mode: ${error instanceof Error ? error.message : String(error)}` 60 | } 61 | ], 62 | isError: true 63 | }; 64 | } 65 | } 66 | ); 67 | 68 | // OpenInputPropertiesDialog tool 69 | server.tool( 70 | "obs-open-input-properties", 71 | "Opens the properties dialog of an input", 72 | { 73 | inputName: z.string().optional().describe("Name of the input to open the dialog of"), 74 | inputUuid: z.string().optional().describe("UUID of the input to open the dialog of") 75 | }, 76 | async ({ inputName, inputUuid }) => { 77 | try { 78 | const params: Record = {}; 79 | 80 | if (inputName !== undefined) { 81 | params.inputName = inputName; 82 | } 83 | if (inputUuid !== undefined) { 84 | params.inputUuid = inputUuid; 85 | } 86 | 87 | await client.sendRequest("OpenInputPropertiesDialog", params); 88 | return { 89 | content: [ 90 | { 91 | type: "text", 92 | text: `Properties dialog opened for input: ${inputName || inputUuid}` 93 | } 94 | ] 95 | }; 96 | } catch (error) { 97 | return { 98 | content: [ 99 | { 100 | type: "text", 101 | text: `Error opening input properties dialog: ${error instanceof Error ? error.message : String(error)}` 102 | } 103 | ], 104 | isError: true 105 | }; 106 | } 107 | } 108 | ); 109 | 110 | // OpenInputFiltersDialog tool 111 | server.tool( 112 | "obs-open-input-filters", 113 | "Opens the filters dialog of an input", 114 | { 115 | inputName: z.string().optional().describe("Name of the input to open the dialog of"), 116 | inputUuid: z.string().optional().describe("UUID of the input to open the dialog of") 117 | }, 118 | async ({ inputName, inputUuid }) => { 119 | try { 120 | const params: Record = {}; 121 | 122 | if (inputName !== undefined) { 123 | params.inputName = inputName; 124 | } 125 | if (inputUuid !== undefined) { 126 | params.inputUuid = inputUuid; 127 | } 128 | 129 | await client.sendRequest("OpenInputFiltersDialog", params); 130 | return { 131 | content: [ 132 | { 133 | type: "text", 134 | text: `Filters dialog opened for input: ${inputName || inputUuid}` 135 | } 136 | ] 137 | }; 138 | } catch (error) { 139 | return { 140 | content: [ 141 | { 142 | type: "text", 143 | text: `Error opening input filters dialog: ${error instanceof Error ? error.message : String(error)}` 144 | } 145 | ], 146 | isError: true 147 | }; 148 | } 149 | } 150 | ); 151 | 152 | // OpenInputInteractDialog tool 153 | server.tool( 154 | "obs-open-input-interact", 155 | "Opens the interact dialog of an input", 156 | { 157 | inputName: z.string().optional().describe("Name of the input to open the dialog of"), 158 | inputUuid: z.string().optional().describe("UUID of the input to open the dialog of") 159 | }, 160 | async ({ inputName, inputUuid }) => { 161 | try { 162 | const params: Record = {}; 163 | 164 | if (inputName !== undefined) { 165 | params.inputName = inputName; 166 | } 167 | if (inputUuid !== undefined) { 168 | params.inputUuid = inputUuid; 169 | } 170 | 171 | await client.sendRequest("OpenInputInteractDialog", params); 172 | return { 173 | content: [ 174 | { 175 | type: "text", 176 | text: `Interact dialog opened for input: ${inputName || inputUuid}` 177 | } 178 | ] 179 | }; 180 | } catch (error) { 181 | return { 182 | content: [ 183 | { 184 | type: "text", 185 | text: `Error opening input interact dialog: ${error instanceof Error ? error.message : String(error)}` 186 | } 187 | ], 188 | isError: true 189 | }; 190 | } 191 | } 192 | ); 193 | 194 | // GetMonitorList tool 195 | server.tool( 196 | "obs-get-monitor-list", 197 | "Gets a list of connected monitors and information about them", 198 | {}, 199 | async () => { 200 | try { 201 | const response = await client.sendRequest("GetMonitorList"); 202 | return { 203 | content: [ 204 | { 205 | type: "text", 206 | text: JSON.stringify(response, null, 2) 207 | } 208 | ] 209 | }; 210 | } catch (error) { 211 | return { 212 | content: [ 213 | { 214 | type: "text", 215 | text: `Error getting monitor list: ${error instanceof Error ? error.message : String(error)}` 216 | } 217 | ], 218 | isError: true 219 | }; 220 | } 221 | } 222 | ); 223 | 224 | // OpenVideoMixProjector tool 225 | server.tool( 226 | "obs-open-video-mix-projector", 227 | "Opens a projector for a specific output video mix", 228 | { 229 | videoMixType: z.enum([ 230 | "OBS_WEBSOCKET_VIDEO_MIX_TYPE_PREVIEW", 231 | "OBS_WEBSOCKET_VIDEO_MIX_TYPE_PROGRAM", 232 | "OBS_WEBSOCKET_VIDEO_MIX_TYPE_MULTIVIEW" 233 | ]).describe("Type of mix to open"), 234 | monitorIndex: z.number().optional().describe("Monitor index, use -1 for windowed mode"), 235 | projectorGeometry: z.string().optional().describe("Size/Position data for a windowed projector") 236 | }, 237 | async ({ videoMixType, monitorIndex, projectorGeometry }) => { 238 | try { 239 | const requestParams: Record = { videoMixType }; 240 | if (monitorIndex !== undefined) { 241 | requestParams.monitorIndex = monitorIndex; 242 | } 243 | if (projectorGeometry !== undefined) { 244 | requestParams.projectorGeometry = projectorGeometry; 245 | } 246 | 247 | await client.sendRequest("OpenVideoMixProjector", requestParams); 248 | return { 249 | content: [ 250 | { 251 | type: "text", 252 | text: `Video mix projector opened for: ${videoMixType}` 253 | } 254 | ] 255 | }; 256 | } catch (error) { 257 | return { 258 | content: [ 259 | { 260 | type: "text", 261 | text: `Error opening video mix projector: ${error instanceof Error ? error.message : String(error)}` 262 | } 263 | ], 264 | isError: true 265 | }; 266 | } 267 | } 268 | ); 269 | 270 | // OpenSourceProjector tool 271 | server.tool( 272 | "obs-open-source-projector", 273 | "Opens a projector for a source", 274 | { 275 | sourceName: z.string().optional().describe("Name of the source to open a projector for"), 276 | sourceUuid: z.string().optional().describe("UUID of the source to open a projector for"), 277 | monitorIndex: z.number().optional().describe("Monitor index, use -1 for windowed mode"), 278 | projectorGeometry: z.string().optional().describe("Size/Position data for a windowed projector") 279 | }, 280 | async ({ sourceName, sourceUuid, monitorIndex, projectorGeometry }) => { 281 | try { 282 | const requestParams: Record = {}; 283 | 284 | if (sourceName !== undefined) { 285 | requestParams.sourceName = sourceName; 286 | } 287 | if (sourceUuid !== undefined) { 288 | requestParams.sourceUuid = sourceUuid; 289 | } 290 | if (monitorIndex !== undefined) { 291 | requestParams.monitorIndex = monitorIndex; 292 | } 293 | if (projectorGeometry !== undefined) { 294 | requestParams.projectorGeometry = projectorGeometry; 295 | } 296 | 297 | await client.sendRequest("OpenSourceProjector", requestParams); 298 | return { 299 | content: [ 300 | { 301 | type: "text", 302 | text: `Source projector opened for: ${sourceName || sourceUuid}` 303 | } 304 | ] 305 | }; 306 | } catch (error) { 307 | return { 308 | content: [ 309 | { 310 | type: "text", 311 | text: `Error opening source projector: ${error instanceof Error ? error.message : String(error)}` 312 | } 313 | ], 314 | isError: true 315 | }; 316 | } 317 | } 318 | ); 319 | } -------------------------------------------------------------------------------- /docs/protocol_split/outputs.json: -------------------------------------------------------------------------------- 1 | { 2 | "requests": [ 3 | { 4 | "description": "Gets the status of the virtualcam output.", 5 | "requestType": "GetVirtualCamStatus", 6 | "complexity": 1, 7 | "rpcVersion": "1", 8 | "deprecated": false, 9 | "initialVersion": "5.0.0", 10 | "category": "outputs", 11 | "requestFields": [], 12 | "responseFields": [ 13 | { 14 | "valueName": "outputActive", 15 | "valueType": "Boolean", 16 | "valueDescription": "Whether the output is active" 17 | } 18 | ] 19 | }, 20 | { 21 | "description": "Toggles the state of the virtualcam output.", 22 | "requestType": "ToggleVirtualCam", 23 | "complexity": 1, 24 | "rpcVersion": "1", 25 | "deprecated": false, 26 | "initialVersion": "5.0.0", 27 | "category": "outputs", 28 | "requestFields": [], 29 | "responseFields": [ 30 | { 31 | "valueName": "outputActive", 32 | "valueType": "Boolean", 33 | "valueDescription": "Whether the output is active" 34 | } 35 | ] 36 | }, 37 | { 38 | "description": "Starts the virtualcam output.", 39 | "requestType": "StartVirtualCam", 40 | "complexity": 1, 41 | "rpcVersion": "1", 42 | "deprecated": false, 43 | "initialVersion": "5.0.0", 44 | "category": "outputs", 45 | "requestFields": [], 46 | "responseFields": [] 47 | }, 48 | { 49 | "description": "Stops the virtualcam output.", 50 | "requestType": "StopVirtualCam", 51 | "complexity": 1, 52 | "rpcVersion": "1", 53 | "deprecated": false, 54 | "initialVersion": "5.0.0", 55 | "category": "outputs", 56 | "requestFields": [], 57 | "responseFields": [] 58 | }, 59 | { 60 | "description": "Gets the status of the replay buffer output.", 61 | "requestType": "GetReplayBufferStatus", 62 | "complexity": 1, 63 | "rpcVersion": "1", 64 | "deprecated": false, 65 | "initialVersion": "5.0.0", 66 | "category": "outputs", 67 | "requestFields": [], 68 | "responseFields": [ 69 | { 70 | "valueName": "outputActive", 71 | "valueType": "Boolean", 72 | "valueDescription": "Whether the output is active" 73 | } 74 | ] 75 | }, 76 | { 77 | "description": "Toggles the state of the replay buffer output.", 78 | "requestType": "ToggleReplayBuffer", 79 | "complexity": 1, 80 | "rpcVersion": "1", 81 | "deprecated": false, 82 | "initialVersion": "5.0.0", 83 | "category": "outputs", 84 | "requestFields": [], 85 | "responseFields": [ 86 | { 87 | "valueName": "outputActive", 88 | "valueType": "Boolean", 89 | "valueDescription": "Whether the output is active" 90 | } 91 | ] 92 | }, 93 | { 94 | "description": "Starts the replay buffer output.", 95 | "requestType": "StartReplayBuffer", 96 | "complexity": 1, 97 | "rpcVersion": "1", 98 | "deprecated": false, 99 | "initialVersion": "5.0.0", 100 | "category": "outputs", 101 | "requestFields": [], 102 | "responseFields": [] 103 | }, 104 | { 105 | "description": "Stops the replay buffer output.", 106 | "requestType": "StopReplayBuffer", 107 | "complexity": 1, 108 | "rpcVersion": "1", 109 | "deprecated": false, 110 | "initialVersion": "5.0.0", 111 | "category": "outputs", 112 | "requestFields": [], 113 | "responseFields": [] 114 | }, 115 | { 116 | "description": "Saves the contents of the replay buffer output.", 117 | "requestType": "SaveReplayBuffer", 118 | "complexity": 1, 119 | "rpcVersion": "1", 120 | "deprecated": false, 121 | "initialVersion": "5.0.0", 122 | "category": "outputs", 123 | "requestFields": [], 124 | "responseFields": [] 125 | }, 126 | { 127 | "description": "Gets the filename of the last replay buffer save file.", 128 | "requestType": "GetLastReplayBufferReplay", 129 | "complexity": 2, 130 | "rpcVersion": "1", 131 | "deprecated": false, 132 | "initialVersion": "5.0.0", 133 | "category": "outputs", 134 | "requestFields": [], 135 | "responseFields": [ 136 | { 137 | "valueName": "savedReplayPath", 138 | "valueType": "String", 139 | "valueDescription": "File path" 140 | } 141 | ] 142 | }, 143 | { 144 | "description": "Gets the list of available outputs.", 145 | "requestType": "GetOutputList", 146 | "complexity": 4, 147 | "rpcVersion": "1", 148 | "deprecated": false, 149 | "initialVersion": "5.0.0", 150 | "category": "outputs", 151 | "requestFields": [], 152 | "responseFields": [ 153 | { 154 | "valueName": "outputs", 155 | "valueType": "Array", 156 | "valueDescription": "Array of outputs" 157 | } 158 | ] 159 | }, 160 | { 161 | "description": "Gets the status of an output.", 162 | "requestType": "GetOutputStatus", 163 | "complexity": 4, 164 | "rpcVersion": "1", 165 | "deprecated": false, 166 | "initialVersion": "5.0.0", 167 | "category": "outputs", 168 | "requestFields": [ 169 | { 170 | "valueName": "outputName", 171 | "valueType": "String", 172 | "valueDescription": "Output name", 173 | "valueRestrictions": null, 174 | "valueOptional": false, 175 | "valueOptionalBehavior": null 176 | } 177 | ], 178 | "responseFields": [ 179 | { 180 | "valueName": "outputActive", 181 | "valueType": "Boolean", 182 | "valueDescription": "Whether the output is active" 183 | }, 184 | { 185 | "valueName": "outputReconnecting", 186 | "valueType": "Boolean", 187 | "valueDescription": "Whether the output is reconnecting" 188 | }, 189 | { 190 | "valueName": "outputTimecode", 191 | "valueType": "String", 192 | "valueDescription": "Current formatted timecode string for the output" 193 | }, 194 | { 195 | "valueName": "outputDuration", 196 | "valueType": "Number", 197 | "valueDescription": "Current duration in milliseconds for the output" 198 | }, 199 | { 200 | "valueName": "outputCongestion", 201 | "valueType": "Number", 202 | "valueDescription": "Congestion of the output" 203 | }, 204 | { 205 | "valueName": "outputBytes", 206 | "valueType": "Number", 207 | "valueDescription": "Number of bytes sent by the output" 208 | }, 209 | { 210 | "valueName": "outputSkippedFrames", 211 | "valueType": "Number", 212 | "valueDescription": "Number of frames skipped by the output's process" 213 | }, 214 | { 215 | "valueName": "outputTotalFrames", 216 | "valueType": "Number", 217 | "valueDescription": "Total number of frames delivered by the output's process" 218 | } 219 | ] 220 | }, 221 | { 222 | "description": "Toggles the status of an output.", 223 | "requestType": "ToggleOutput", 224 | "complexity": 4, 225 | "rpcVersion": "1", 226 | "deprecated": false, 227 | "initialVersion": "5.0.0", 228 | "category": "outputs", 229 | "requestFields": [ 230 | { 231 | "valueName": "outputName", 232 | "valueType": "String", 233 | "valueDescription": "Output name", 234 | "valueRestrictions": null, 235 | "valueOptional": false, 236 | "valueOptionalBehavior": null 237 | } 238 | ], 239 | "responseFields": [ 240 | { 241 | "valueName": "outputActive", 242 | "valueType": "Boolean", 243 | "valueDescription": "Whether the output is active" 244 | } 245 | ] 246 | }, 247 | { 248 | "description": "Starts an output.", 249 | "requestType": "StartOutput", 250 | "complexity": 4, 251 | "rpcVersion": "1", 252 | "deprecated": false, 253 | "initialVersion": "5.0.0", 254 | "category": "outputs", 255 | "requestFields": [ 256 | { 257 | "valueName": "outputName", 258 | "valueType": "String", 259 | "valueDescription": "Output name", 260 | "valueRestrictions": null, 261 | "valueOptional": false, 262 | "valueOptionalBehavior": null 263 | } 264 | ], 265 | "responseFields": [] 266 | }, 267 | { 268 | "description": "Stops an output.", 269 | "requestType": "StopOutput", 270 | "complexity": 4, 271 | "rpcVersion": "1", 272 | "deprecated": false, 273 | "initialVersion": "5.0.0", 274 | "category": "outputs", 275 | "requestFields": [ 276 | { 277 | "valueName": "outputName", 278 | "valueType": "String", 279 | "valueDescription": "Output name", 280 | "valueRestrictions": null, 281 | "valueOptional": false, 282 | "valueOptionalBehavior": null 283 | } 284 | ], 285 | "responseFields": [] 286 | }, 287 | { 288 | "description": "Gets the settings of an output.", 289 | "requestType": "GetOutputSettings", 290 | "complexity": 4, 291 | "rpcVersion": "1", 292 | "deprecated": false, 293 | "initialVersion": "5.0.0", 294 | "category": "outputs", 295 | "requestFields": [ 296 | { 297 | "valueName": "outputName", 298 | "valueType": "String", 299 | "valueDescription": "Output name", 300 | "valueRestrictions": null, 301 | "valueOptional": false, 302 | "valueOptionalBehavior": null 303 | } 304 | ], 305 | "responseFields": [ 306 | { 307 | "valueName": "outputSettings", 308 | "valueType": "Object", 309 | "valueDescription": "Output settings" 310 | } 311 | ] 312 | }, 313 | { 314 | "description": "Sets the settings of an output.", 315 | "requestType": "SetOutputSettings", 316 | "complexity": 4, 317 | "rpcVersion": "1", 318 | "deprecated": false, 319 | "initialVersion": "5.0.0", 320 | "category": "outputs", 321 | "requestFields": [ 322 | { 323 | "valueName": "outputName", 324 | "valueType": "String", 325 | "valueDescription": "Output name", 326 | "valueRestrictions": null, 327 | "valueOptional": false, 328 | "valueOptionalBehavior": null 329 | }, 330 | { 331 | "valueName": "outputSettings", 332 | "valueType": "Object", 333 | "valueDescription": "Output settings", 334 | "valueRestrictions": null, 335 | "valueOptional": false, 336 | "valueOptionalBehavior": null 337 | } 338 | ], 339 | "responseFields": [] 340 | } 341 | ] 342 | } -------------------------------------------------------------------------------- /src/tools/general.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { OBSWebSocketClient } from "../client.js"; 3 | import { z } from "zod"; 4 | 5 | export async function initialize(server: McpServer, client: OBSWebSocketClient): Promise { 6 | // Get server status 7 | server.tool( 8 | "obs-get-status", 9 | "Get the current status of the OBS MCP server and OBS connection", 10 | {}, 11 | async () => { 12 | const status = client.getConnectionStatus(); 13 | const obsConnected = client.isConnected(); 14 | 15 | const statusInfo = { 16 | server: { 17 | name: "obs-mcp", 18 | version: "1.0.1", 19 | status: "running" 20 | }, 21 | obs: { 22 | connected: obsConnected, 23 | url: status.url, 24 | hasPassword: status.hasPassword, 25 | identified: status.identified 26 | }, 27 | timestamp: new Date().toISOString() 28 | }; 29 | 30 | return { 31 | content: [ 32 | { 33 | type: "text", 34 | text: JSON.stringify(statusInfo, null, 2) 35 | } 36 | ] 37 | }; 38 | } 39 | ); 40 | 41 | // Get OBS version info 42 | server.tool( 43 | "obs-get-version", 44 | "Get OBS Studio version information", 45 | {}, 46 | async () => { 47 | if (!client.isConnected()) { 48 | return { 49 | content: [ 50 | { 51 | type: "text", 52 | text: "Not connected to OBS WebSocket" 53 | } 54 | ], 55 | isError: true 56 | }; 57 | } 58 | 59 | try { 60 | const version = await client.sendRequest("GetVersion"); 61 | return { 62 | content: [ 63 | { 64 | type: "text", 65 | text: JSON.stringify(version, null, 2) 66 | } 67 | ] 68 | }; 69 | } catch (error) { 70 | return { 71 | content: [ 72 | { 73 | type: "text", 74 | text: `Failed to get OBS version: ${error instanceof Error ? error.message : String(error)}` 75 | } 76 | ], 77 | isError: true 78 | }; 79 | } 80 | } 81 | ); 82 | 83 | // Test OBS connection 84 | server.tool( 85 | "obs-test-connection", 86 | "Test the connection to OBS WebSocket", 87 | {}, 88 | async () => { 89 | if (!client.isConnected()) { 90 | return { 91 | content: [ 92 | { 93 | type: "text", 94 | text: "Not connected to OBS WebSocket" 95 | } 96 | ], 97 | isError: true 98 | }; 99 | } 100 | 101 | try { 102 | // Try a simple request to test the connection 103 | await client.sendRequest("GetVersion"); 104 | return { 105 | content: [ 106 | { 107 | type: "text", 108 | text: "Connection test successful - OBS is responding" 109 | } 110 | ] 111 | }; 112 | } catch (error) { 113 | return { 114 | content: [ 115 | { 116 | type: "text", 117 | text: `Connection test failed: ${error instanceof Error ? error.message : String(error)}` 118 | } 119 | ], 120 | isError: true 121 | }; 122 | } 123 | } 124 | ); 125 | 126 | // GetStats tool 127 | server.tool( 128 | "obs-get-stats", 129 | "Gets statistics about OBS, obs-websocket, and the current session", 130 | {}, 131 | async () => { 132 | try { 133 | const stats = await client.sendRequest("GetStats"); 134 | return { 135 | content: [ 136 | { 137 | type: "text", 138 | text: JSON.stringify(stats, null, 2) 139 | } 140 | ] 141 | }; 142 | } catch (error) { 143 | return { 144 | content: [ 145 | { 146 | type: "text", 147 | text: `Error getting stats: ${error instanceof Error ? error.message : String(error)}` 148 | } 149 | ], 150 | isError: true 151 | }; 152 | } 153 | } 154 | ); 155 | 156 | // BroadcastCustomEvent tool 157 | server.tool( 158 | "obs-broadcast-custom-event", 159 | "Broadcasts a CustomEvent to all WebSocket clients", 160 | { 161 | eventData: z.record(z.any()).describe("Data payload to emit to all receivers") 162 | }, 163 | async ({ eventData }) => { 164 | try { 165 | await client.sendRequest("BroadcastCustomEvent", { eventData }); 166 | return { 167 | content: [ 168 | { 169 | type: "text", 170 | text: "Custom event broadcast successfully" 171 | } 172 | ] 173 | }; 174 | } catch (error) { 175 | return { 176 | content: [ 177 | { 178 | type: "text", 179 | text: `Error broadcasting custom event: ${error instanceof Error ? error.message : String(error)}` 180 | } 181 | ], 182 | isError: true 183 | }; 184 | } 185 | } 186 | ); 187 | 188 | // CallVendorRequest tool 189 | server.tool( 190 | "obs-call-vendor-request", 191 | "Call a request registered to a vendor", 192 | { 193 | vendorName: z.string().describe("Name of the vendor to use"), 194 | requestType: z.string().describe("The request type to call"), 195 | requestData: z.record(z.any()).optional().describe("Object containing appropriate request data") 196 | }, 197 | async ({ vendorName, requestType, requestData }) => { 198 | try { 199 | const params: Record = { 200 | vendorName, 201 | requestType 202 | }; 203 | 204 | if (requestData !== undefined) { 205 | params.requestData = requestData; 206 | } 207 | 208 | const response = await client.sendRequest("CallVendorRequest", params); 209 | return { 210 | content: [ 211 | { 212 | type: "text", 213 | text: JSON.stringify(response, null, 2) 214 | } 215 | ] 216 | }; 217 | } catch (error) { 218 | return { 219 | content: [ 220 | { 221 | type: "text", 222 | text: `Error calling vendor request: ${error instanceof Error ? error.message : String(error)}` 223 | } 224 | ], 225 | isError: true 226 | }; 227 | } 228 | } 229 | ); 230 | 231 | // GetHotkeyList tool 232 | server.tool( 233 | "obs-get-hotkey-list", 234 | "Gets an array of all hotkey names in OBS", 235 | {}, 236 | async () => { 237 | try { 238 | const hotkeyList = await client.sendRequest("GetHotkeyList"); 239 | return { 240 | content: [ 241 | { 242 | type: "text", 243 | text: JSON.stringify(hotkeyList, null, 2) 244 | } 245 | ] 246 | }; 247 | } catch (error) { 248 | return { 249 | content: [ 250 | { 251 | type: "text", 252 | text: `Error getting hotkey list: ${error instanceof Error ? error.message : String(error)}` 253 | } 254 | ], 255 | isError: true 256 | }; 257 | } 258 | } 259 | ); 260 | 261 | // TriggerHotkeyByName tool 262 | server.tool( 263 | "obs-trigger-hotkey-by-name", 264 | "Triggers a hotkey using its name", 265 | { 266 | hotkeyName: z.string().describe("Name of the hotkey to trigger"), 267 | contextName: z.string().optional().describe("Name of context of the hotkey to trigger") 268 | }, 269 | async ({ hotkeyName, contextName }) => { 270 | try { 271 | const params: Record = { hotkeyName }; 272 | 273 | if (contextName !== undefined) { 274 | params.contextName = contextName; 275 | } 276 | 277 | await client.sendRequest("TriggerHotkeyByName", params); 278 | return { 279 | content: [ 280 | { 281 | type: "text", 282 | text: `Successfully triggered hotkey: ${hotkeyName}` 283 | } 284 | ] 285 | }; 286 | } catch (error) { 287 | return { 288 | content: [ 289 | { 290 | type: "text", 291 | text: `Error triggering hotkey: ${error instanceof Error ? error.message : String(error)}` 292 | } 293 | ], 294 | isError: true 295 | }; 296 | } 297 | } 298 | ); 299 | 300 | // TriggerHotkeyByKeySequence tool 301 | server.tool( 302 | "obs-trigger-hotkey-by-key-sequence", 303 | "Triggers a hotkey using a sequence of keys", 304 | { 305 | keyId: z.string().optional().describe("The OBS key ID to use"), 306 | keyModifiers: z.object({ 307 | shift: z.boolean().optional().describe("Press Shift"), 308 | control: z.boolean().optional().describe("Press CTRL"), 309 | alt: z.boolean().optional().describe("Press ALT"), 310 | command: z.boolean().optional().describe("Press CMD (Mac)") 311 | }).optional().describe("Object containing key modifiers to apply") 312 | }, 313 | async ({ keyId, keyModifiers }) => { 314 | try { 315 | const params: Record = {}; 316 | 317 | if (keyId !== undefined) { 318 | params.keyId = keyId; 319 | } 320 | 321 | if (keyModifiers !== undefined) { 322 | params.keyModifiers = keyModifiers; 323 | } 324 | 325 | await client.sendRequest("TriggerHotkeyByKeySequence", params); 326 | return { 327 | content: [ 328 | { 329 | type: "text", 330 | text: "Hotkey triggered by key sequence successfully" 331 | } 332 | ] 333 | }; 334 | } catch (error) { 335 | return { 336 | content: [ 337 | { 338 | type: "text", 339 | text: `Error triggering hotkey by key sequence: ${error instanceof Error ? error.message : String(error)}` 340 | } 341 | ], 342 | isError: true 343 | }; 344 | } 345 | } 346 | ); 347 | 348 | // Sleep tool 349 | server.tool( 350 | "obs-sleep", 351 | "Sleeps for a time duration or number of frames", 352 | { 353 | sleepMillis: z.number().optional().describe("Number of milliseconds to sleep for"), 354 | sleepFrames: z.number().optional().describe("Number of frames to sleep for") 355 | }, 356 | async ({ sleepMillis, sleepFrames }) => { 357 | try { 358 | const params: Record = {}; 359 | 360 | if (sleepMillis !== undefined) { 361 | params.sleepMillis = sleepMillis; 362 | } 363 | 364 | if (sleepFrames !== undefined) { 365 | params.sleepFrames = sleepFrames; 366 | } 367 | 368 | await client.sendRequest("Sleep", params); 369 | return { 370 | content: [ 371 | { 372 | type: "text", 373 | text: "Sleep operation completed successfully" 374 | } 375 | ] 376 | }; 377 | } catch (error) { 378 | return { 379 | content: [ 380 | { 381 | type: "text", 382 | text: `Error during sleep operation: ${error instanceof Error ? error.message : String(error)}` 383 | } 384 | ], 385 | isError: true 386 | }; 387 | } 388 | } 389 | ); 390 | } -------------------------------------------------------------------------------- /src/tools/filters.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { OBSWebSocketClient } from "../client.js"; 3 | import { z } from "zod"; 4 | 5 | export async function initialize(server: McpServer, client: OBSWebSocketClient): Promise { 6 | // GetSourceFilterKindList tool 7 | server.tool( 8 | "obs-get-filter-kind-list", 9 | "Gets an array of all available source filter kinds", 10 | {}, 11 | async () => { 12 | try { 13 | const response = await client.sendRequest("GetSourceFilterKindList"); 14 | return { 15 | content: [ 16 | { 17 | type: "text", 18 | text: JSON.stringify(response, null, 2) 19 | } 20 | ] 21 | }; 22 | } catch (error) { 23 | return { 24 | content: [ 25 | { 26 | type: "text", 27 | text: `Error getting filter kind list: ${error instanceof Error ? error.message : String(error)}` 28 | } 29 | ], 30 | isError: true 31 | }; 32 | } 33 | } 34 | ); 35 | 36 | // GetSourceFilterList tool 37 | server.tool( 38 | "obs-get-source-filter-list", 39 | "Gets an array of all of a source's filters", 40 | { 41 | sourceName: z.string().describe("Name of the source") 42 | }, 43 | async ({ sourceName }) => { 44 | try { 45 | const response = await client.sendRequest("GetSourceFilterList", { sourceName }); 46 | return { 47 | content: [ 48 | { 49 | type: "text", 50 | text: JSON.stringify(response, null, 2) 51 | } 52 | ] 53 | }; 54 | } catch (error) { 55 | return { 56 | content: [ 57 | { 58 | type: "text", 59 | text: `Error getting source filter list: ${error instanceof Error ? error.message : String(error)}` 60 | } 61 | ], 62 | isError: true 63 | }; 64 | } 65 | } 66 | ); 67 | 68 | // GetSourceFilterDefaultSettings tool 69 | server.tool( 70 | "obs-get-filter-default-settings", 71 | "Gets the default settings for a filter kind", 72 | { 73 | filterKind: z.string().describe("Filter kind to get the default settings for") 74 | }, 75 | async ({ filterKind }) => { 76 | try { 77 | const response = await client.sendRequest("GetSourceFilterDefaultSettings", { filterKind }); 78 | return { 79 | content: [ 80 | { 81 | type: "text", 82 | text: JSON.stringify(response, null, 2) 83 | } 84 | ] 85 | }; 86 | } catch (error) { 87 | return { 88 | content: [ 89 | { 90 | type: "text", 91 | text: `Error getting filter default settings: ${error instanceof Error ? error.message : String(error)}` 92 | } 93 | ], 94 | isError: true 95 | }; 96 | } 97 | } 98 | ); 99 | 100 | // CreateSourceFilter tool 101 | server.tool( 102 | "obs-create-source-filter", 103 | "Creates a new filter, adding it to the specified source", 104 | { 105 | sourceName: z.string().describe("Name of the source to add the filter to"), 106 | filterName: z.string().describe("Name of the new filter to be created"), 107 | filterKind: z.string().describe("The kind of filter to be created"), 108 | filterSettings: z.record(z.any()).optional().describe("Settings object to initialize the filter with") 109 | }, 110 | async ({ sourceName, filterName, filterKind, filterSettings }) => { 111 | try { 112 | const requestParams: Record = { sourceName, filterName, filterKind }; 113 | if (filterSettings !== undefined) { 114 | requestParams.filterSettings = filterSettings; 115 | } 116 | 117 | await client.sendRequest("CreateSourceFilter", requestParams); 118 | return { 119 | content: [ 120 | { 121 | type: "text", 122 | text: `Successfully created filter '${filterName}' of kind '${filterKind}' on source '${sourceName}'` 123 | } 124 | ] 125 | }; 126 | } catch (error) { 127 | return { 128 | content: [ 129 | { 130 | type: "text", 131 | text: `Error creating source filter: ${error instanceof Error ? error.message : String(error)}` 132 | } 133 | ], 134 | isError: true 135 | }; 136 | } 137 | } 138 | ); 139 | 140 | // RemoveSourceFilter tool 141 | server.tool( 142 | "obs-remove-source-filter", 143 | "Removes a filter from a source", 144 | { 145 | sourceName: z.string().describe("Name of the source the filter is on"), 146 | filterName: z.string().describe("Name of the filter to remove") 147 | }, 148 | async ({ sourceName, filterName }) => { 149 | try { 150 | await client.sendRequest("RemoveSourceFilter", { sourceName, filterName }); 151 | return { 152 | content: [ 153 | { 154 | type: "text", 155 | text: `Successfully removed filter '${filterName}' from source '${sourceName}'` 156 | } 157 | ] 158 | }; 159 | } catch (error) { 160 | return { 161 | content: [ 162 | { 163 | type: "text", 164 | text: `Error removing source filter: ${error instanceof Error ? error.message : String(error)}` 165 | } 166 | ], 167 | isError: true 168 | }; 169 | } 170 | } 171 | ); 172 | 173 | // SetSourceFilterName tool 174 | server.tool( 175 | "obs-set-source-filter-name", 176 | "Sets the name of a source filter (rename)", 177 | { 178 | sourceName: z.string().describe("Name of the source the filter is on"), 179 | filterName: z.string().describe("Current name of the filter"), 180 | newFilterName: z.string().describe("New name for the filter") 181 | }, 182 | async ({ sourceName, filterName, newFilterName }) => { 183 | try { 184 | await client.sendRequest("SetSourceFilterName", { sourceName, filterName, newFilterName }); 185 | return { 186 | content: [ 187 | { 188 | type: "text", 189 | text: `Successfully renamed filter '${filterName}' to '${newFilterName}' on source '${sourceName}'` 190 | } 191 | ] 192 | }; 193 | } catch (error) { 194 | return { 195 | content: [ 196 | { 197 | type: "text", 198 | text: `Error renaming source filter: ${error instanceof Error ? error.message : String(error)}` 199 | } 200 | ], 201 | isError: true 202 | }; 203 | } 204 | } 205 | ); 206 | 207 | // GetSourceFilter tool 208 | server.tool( 209 | "obs-get-source-filter", 210 | "Gets the info for a specific source filter", 211 | { 212 | sourceName: z.string().describe("Name of the source"), 213 | filterName: z.string().describe("Name of the filter") 214 | }, 215 | async ({ sourceName, filterName }) => { 216 | try { 217 | const response = await client.sendRequest("GetSourceFilter", { sourceName, filterName }); 218 | return { 219 | content: [ 220 | { 221 | type: "text", 222 | text: JSON.stringify(response, null, 2) 223 | } 224 | ] 225 | }; 226 | } catch (error) { 227 | return { 228 | content: [ 229 | { 230 | type: "text", 231 | text: `Error getting source filter info: ${error instanceof Error ? error.message : String(error)}` 232 | } 233 | ], 234 | isError: true 235 | }; 236 | } 237 | } 238 | ); 239 | 240 | // SetSourceFilterIndex tool 241 | server.tool( 242 | "obs-set-source-filter-index", 243 | "Sets the index position of a filter on a source", 244 | { 245 | sourceName: z.string().describe("Name of the source the filter is on"), 246 | filterName: z.string().describe("Name of the filter"), 247 | filterIndex: z.number().min(0).describe("New index position of the filter") 248 | }, 249 | async ({ sourceName, filterName, filterIndex }) => { 250 | try { 251 | await client.sendRequest("SetSourceFilterIndex", { sourceName, filterName, filterIndex }); 252 | return { 253 | content: [ 254 | { 255 | type: "text", 256 | text: `Successfully set filter '${filterName}' to index ${filterIndex} on source '${sourceName}'` 257 | } 258 | ] 259 | }; 260 | } catch (error) { 261 | return { 262 | content: [ 263 | { 264 | type: "text", 265 | text: `Error setting source filter index: ${error instanceof Error ? error.message : String(error)}` 266 | } 267 | ], 268 | isError: true 269 | }; 270 | } 271 | } 272 | ); 273 | 274 | // SetSourceFilterSettings tool 275 | server.tool( 276 | "obs-set-source-filter-settings", 277 | "Sets the settings of a source filter", 278 | { 279 | sourceName: z.string().describe("Name of the source the filter is on"), 280 | filterName: z.string().describe("Name of the filter to set the settings of"), 281 | filterSettings: z.record(z.any()).describe("Object of settings to apply"), 282 | overlay: z.boolean().optional().describe("True to apply settings on top of existing ones, False to reset to defaults first") 283 | }, 284 | async ({ sourceName, filterName, filterSettings, overlay }) => { 285 | try { 286 | const requestParams: Record = { sourceName, filterName, filterSettings }; 287 | if (overlay !== undefined) { 288 | requestParams.overlay = overlay; 289 | } 290 | 291 | await client.sendRequest("SetSourceFilterSettings", requestParams); 292 | return { 293 | content: [ 294 | { 295 | type: "text", 296 | text: `Successfully updated settings for filter '${filterName}' on source '${sourceName}'` 297 | } 298 | ] 299 | }; 300 | } catch (error) { 301 | return { 302 | content: [ 303 | { 304 | type: "text", 305 | text: `Error setting source filter settings: ${error instanceof Error ? error.message : String(error)}` 306 | } 307 | ], 308 | isError: true 309 | }; 310 | } 311 | } 312 | ); 313 | 314 | // SetSourceFilterEnabled tool 315 | server.tool( 316 | "obs-set-source-filter-enabled", 317 | "Sets the enable state of a source filter", 318 | { 319 | sourceName: z.string().describe("Name of the source the filter is on"), 320 | filterName: z.string().describe("Name of the filter"), 321 | filterEnabled: z.boolean().describe("New enable state of the filter") 322 | }, 323 | async ({ sourceName, filterName, filterEnabled }) => { 324 | try { 325 | await client.sendRequest("SetSourceFilterEnabled", { sourceName, filterName, filterEnabled }); 326 | return { 327 | content: [ 328 | { 329 | type: "text", 330 | text: `Successfully ${filterEnabled ? "enabled" : "disabled"} filter '${filterName}' on source '${sourceName}'` 331 | } 332 | ] 333 | }; 334 | } catch (error) { 335 | return { 336 | content: [ 337 | { 338 | type: "text", 339 | text: `Error setting source filter enabled state: ${error instanceof Error ? error.message : String(error)}` 340 | } 341 | ], 342 | isError: true 343 | }; 344 | } 345 | } 346 | ); 347 | } -------------------------------------------------------------------------------- /docs/protocol_split/scenes.json: -------------------------------------------------------------------------------- 1 | { 2 | "requests": [ 3 | { 4 | "description": "Gets an array of all scenes in OBS.", 5 | "requestType": "GetSceneList", 6 | "complexity": 2, 7 | "rpcVersion": "1", 8 | "deprecated": false, 9 | "initialVersion": "5.0.0", 10 | "category": "scenes", 11 | "requestFields": [], 12 | "responseFields": [ 13 | { 14 | "valueName": "currentProgramSceneName", 15 | "valueType": "String", 16 | "valueDescription": "Current program scene name. Can be `null` if internal state desync" 17 | }, 18 | { 19 | "valueName": "currentProgramSceneUuid", 20 | "valueType": "String", 21 | "valueDescription": "Current program scene UUID. Can be `null` if internal state desync" 22 | }, 23 | { 24 | "valueName": "currentPreviewSceneName", 25 | "valueType": "String", 26 | "valueDescription": "Current preview scene name. `null` if not in studio mode" 27 | }, 28 | { 29 | "valueName": "currentPreviewSceneUuid", 30 | "valueType": "String", 31 | "valueDescription": "Current preview scene UUID. `null` if not in studio mode" 32 | }, 33 | { 34 | "valueName": "scenes", 35 | "valueType": "Array", 36 | "valueDescription": "Array of scenes" 37 | } 38 | ] 39 | }, 40 | { 41 | "description": "Gets an array of all groups in OBS.\n\nGroups in OBS are actually scenes, but renamed and modified. In obs-websocket, we treat them as scenes where we can.", 42 | "requestType": "GetGroupList", 43 | "complexity": 2, 44 | "rpcVersion": "1", 45 | "deprecated": false, 46 | "initialVersion": "5.0.0", 47 | "category": "scenes", 48 | "requestFields": [], 49 | "responseFields": [ 50 | { 51 | "valueName": "groups", 52 | "valueType": "Array", 53 | "valueDescription": "Array of group names" 54 | } 55 | ] 56 | }, 57 | { 58 | "description": "Gets the current program scene.\n\nNote: This request is slated to have the `currentProgram`-prefixed fields removed from in an upcoming RPC version.", 59 | "requestType": "GetCurrentProgramScene", 60 | "complexity": 1, 61 | "rpcVersion": "1", 62 | "deprecated": false, 63 | "initialVersion": "5.0.0", 64 | "category": "scenes", 65 | "requestFields": [], 66 | "responseFields": [ 67 | { 68 | "valueName": "sceneName", 69 | "valueType": "String", 70 | "valueDescription": "Current program scene name" 71 | }, 72 | { 73 | "valueName": "sceneUuid", 74 | "valueType": "String", 75 | "valueDescription": "Current program scene UUID" 76 | }, 77 | { 78 | "valueName": "currentProgramSceneName", 79 | "valueType": "String", 80 | "valueDescription": "Current program scene name (Deprecated)" 81 | }, 82 | { 83 | "valueName": "currentProgramSceneUuid", 84 | "valueType": "String", 85 | "valueDescription": "Current program scene UUID (Deprecated)" 86 | } 87 | ] 88 | }, 89 | { 90 | "description": "Sets the current program scene.", 91 | "requestType": "SetCurrentProgramScene", 92 | "complexity": 1, 93 | "rpcVersion": "1", 94 | "deprecated": false, 95 | "initialVersion": "5.0.0", 96 | "category": "scenes", 97 | "requestFields": [ 98 | { 99 | "valueName": "sceneName", 100 | "valueType": "String", 101 | "valueDescription": "Scene name to set as the current program scene", 102 | "valueRestrictions": null, 103 | "valueOptional": true, 104 | "valueOptionalBehavior": "Unknown" 105 | }, 106 | { 107 | "valueName": "sceneUuid", 108 | "valueType": "String", 109 | "valueDescription": "Scene UUID to set as the current program scene", 110 | "valueRestrictions": null, 111 | "valueOptional": true, 112 | "valueOptionalBehavior": "Unknown" 113 | } 114 | ], 115 | "responseFields": [] 116 | }, 117 | { 118 | "description": "Gets the current preview scene.\n\nOnly available when studio mode is enabled.\n\nNote: This request is slated to have the `currentPreview`-prefixed fields removed from in an upcoming RPC version.", 119 | "requestType": "GetCurrentPreviewScene", 120 | "complexity": 1, 121 | "rpcVersion": "1", 122 | "deprecated": false, 123 | "initialVersion": "5.0.0", 124 | "category": "scenes", 125 | "requestFields": [], 126 | "responseFields": [ 127 | { 128 | "valueName": "sceneName", 129 | "valueType": "String", 130 | "valueDescription": "Current preview scene name" 131 | }, 132 | { 133 | "valueName": "sceneUuid", 134 | "valueType": "String", 135 | "valueDescription": "Current preview scene UUID" 136 | }, 137 | { 138 | "valueName": "currentPreviewSceneName", 139 | "valueType": "String", 140 | "valueDescription": "Current preview scene name" 141 | }, 142 | { 143 | "valueName": "currentPreviewSceneUuid", 144 | "valueType": "String", 145 | "valueDescription": "Current preview scene UUID" 146 | } 147 | ] 148 | }, 149 | { 150 | "description": "Sets the current preview scene.\n\nOnly available when studio mode is enabled.", 151 | "requestType": "SetCurrentPreviewScene", 152 | "complexity": 1, 153 | "rpcVersion": "1", 154 | "deprecated": false, 155 | "initialVersion": "5.0.0", 156 | "category": "scenes", 157 | "requestFields": [ 158 | { 159 | "valueName": "sceneName", 160 | "valueType": "String", 161 | "valueDescription": "Scene name to set as the current preview scene", 162 | "valueRestrictions": null, 163 | "valueOptional": true, 164 | "valueOptionalBehavior": "Unknown" 165 | }, 166 | { 167 | "valueName": "sceneUuid", 168 | "valueType": "String", 169 | "valueDescription": "Scene UUID to set as the current preview scene", 170 | "valueRestrictions": null, 171 | "valueOptional": true, 172 | "valueOptionalBehavior": "Unknown" 173 | } 174 | ], 175 | "responseFields": [] 176 | }, 177 | { 178 | "description": "Creates a new scene in OBS.", 179 | "requestType": "CreateScene", 180 | "complexity": 2, 181 | "rpcVersion": "1", 182 | "deprecated": false, 183 | "initialVersion": "5.0.0", 184 | "category": "scenes", 185 | "requestFields": [ 186 | { 187 | "valueName": "sceneName", 188 | "valueType": "String", 189 | "valueDescription": "Name for the new scene", 190 | "valueRestrictions": null, 191 | "valueOptional": false, 192 | "valueOptionalBehavior": null 193 | } 194 | ], 195 | "responseFields": [ 196 | { 197 | "valueName": "sceneUuid", 198 | "valueType": "String", 199 | "valueDescription": "UUID of the created scene" 200 | } 201 | ] 202 | }, 203 | { 204 | "description": "Removes a scene from OBS.", 205 | "requestType": "RemoveScene", 206 | "complexity": 2, 207 | "rpcVersion": "1", 208 | "deprecated": false, 209 | "initialVersion": "5.0.0", 210 | "category": "scenes", 211 | "requestFields": [ 212 | { 213 | "valueName": "sceneName", 214 | "valueType": "String", 215 | "valueDescription": "Name of the scene to remove", 216 | "valueRestrictions": null, 217 | "valueOptional": true, 218 | "valueOptionalBehavior": "Unknown" 219 | }, 220 | { 221 | "valueName": "sceneUuid", 222 | "valueType": "String", 223 | "valueDescription": "UUID of the scene to remove", 224 | "valueRestrictions": null, 225 | "valueOptional": true, 226 | "valueOptionalBehavior": "Unknown" 227 | } 228 | ], 229 | "responseFields": [] 230 | }, 231 | { 232 | "description": "Sets the name of a scene (rename).", 233 | "requestType": "SetSceneName", 234 | "complexity": 2, 235 | "rpcVersion": "1", 236 | "deprecated": false, 237 | "initialVersion": "5.0.0", 238 | "category": "scenes", 239 | "requestFields": [ 240 | { 241 | "valueName": "sceneName", 242 | "valueType": "String", 243 | "valueDescription": "Name of the scene to be renamed", 244 | "valueRestrictions": null, 245 | "valueOptional": true, 246 | "valueOptionalBehavior": "Unknown" 247 | }, 248 | { 249 | "valueName": "sceneUuid", 250 | "valueType": "String", 251 | "valueDescription": "UUID of the scene to be renamed", 252 | "valueRestrictions": null, 253 | "valueOptional": true, 254 | "valueOptionalBehavior": "Unknown" 255 | }, 256 | { 257 | "valueName": "newSceneName", 258 | "valueType": "String", 259 | "valueDescription": "New name for the scene", 260 | "valueRestrictions": null, 261 | "valueOptional": false, 262 | "valueOptionalBehavior": null 263 | } 264 | ], 265 | "responseFields": [] 266 | }, 267 | { 268 | "description": "Gets the scene transition overridden for a scene.\n\nNote: A transition UUID response field is not currently able to be implemented as of 2024-1-18.", 269 | "requestType": "GetSceneSceneTransitionOverride", 270 | "complexity": 2, 271 | "rpcVersion": "1", 272 | "deprecated": false, 273 | "initialVersion": "5.0.0", 274 | "category": "scenes", 275 | "requestFields": [ 276 | { 277 | "valueName": "sceneName", 278 | "valueType": "String", 279 | "valueDescription": "Name of the scene", 280 | "valueRestrictions": null, 281 | "valueOptional": true, 282 | "valueOptionalBehavior": "Unknown" 283 | }, 284 | { 285 | "valueName": "sceneUuid", 286 | "valueType": "String", 287 | "valueDescription": "UUID of the scene", 288 | "valueRestrictions": null, 289 | "valueOptional": true, 290 | "valueOptionalBehavior": "Unknown" 291 | } 292 | ], 293 | "responseFields": [ 294 | { 295 | "valueName": "transitionName", 296 | "valueType": "String", 297 | "valueDescription": "Name of the overridden scene transition, else `null`" 298 | }, 299 | { 300 | "valueName": "transitionDuration", 301 | "valueType": "Number", 302 | "valueDescription": "Duration of the overridden scene transition, else `null`" 303 | } 304 | ] 305 | }, 306 | { 307 | "description": "Sets the scene transition overridden for a scene.", 308 | "requestType": "SetSceneSceneTransitionOverride", 309 | "complexity": 2, 310 | "rpcVersion": "1", 311 | "deprecated": false, 312 | "initialVersion": "5.0.0", 313 | "category": "scenes", 314 | "requestFields": [ 315 | { 316 | "valueName": "sceneName", 317 | "valueType": "String", 318 | "valueDescription": "Name of the scene", 319 | "valueRestrictions": null, 320 | "valueOptional": true, 321 | "valueOptionalBehavior": "Unknown" 322 | }, 323 | { 324 | "valueName": "sceneUuid", 325 | "valueType": "String", 326 | "valueDescription": "UUID of the scene", 327 | "valueRestrictions": null, 328 | "valueOptional": true, 329 | "valueOptionalBehavior": "Unknown" 330 | }, 331 | { 332 | "valueName": "transitionName", 333 | "valueType": "String", 334 | "valueDescription": "Name of the scene transition to use as override. Specify `null` to remove", 335 | "valueRestrictions": null, 336 | "valueOptional": true, 337 | "valueOptionalBehavior": "Unchanged" 338 | }, 339 | { 340 | "valueName": "transitionDuration", 341 | "valueType": "Number", 342 | "valueDescription": "Duration to use for any overridden transition. Specify `null` to remove", 343 | "valueRestrictions": ">= 50, <= 20000", 344 | "valueOptional": true, 345 | "valueOptionalBehavior": "Unchanged" 346 | } 347 | ], 348 | "responseFields": [] 349 | } 350 | ] 351 | } -------------------------------------------------------------------------------- /docs/protocol_split/general.json: -------------------------------------------------------------------------------- 1 | { 2 | "requests": [ 3 | { 4 | "description": "Gets data about the current plugin and RPC version.", 5 | "requestType": "GetVersion", 6 | "complexity": 1, 7 | "rpcVersion": "1", 8 | "deprecated": false, 9 | "initialVersion": "5.0.0", 10 | "category": "general", 11 | "requestFields": [], 12 | "responseFields": [ 13 | { 14 | "valueName": "obsVersion", 15 | "valueType": "String", 16 | "valueDescription": "Current OBS Studio version" 17 | }, 18 | { 19 | "valueName": "obsWebSocketVersion", 20 | "valueType": "String", 21 | "valueDescription": "Current obs-websocket version" 22 | }, 23 | { 24 | "valueName": "rpcVersion", 25 | "valueType": "Number", 26 | "valueDescription": "Current latest obs-websocket RPC version" 27 | }, 28 | { 29 | "valueName": "availableRequests", 30 | "valueType": "Array", 31 | "valueDescription": "Array of available RPC requests for the currently negotiated RPC version" 32 | }, 33 | { 34 | "valueName": "supportedImageFormats", 35 | "valueType": "Array", 36 | "valueDescription": "Image formats available in `GetSourceScreenshot` and `SaveSourceScreenshot` requests." 37 | }, 38 | { 39 | "valueName": "platform", 40 | "valueType": "String", 41 | "valueDescription": "Name of the platform. Usually `windows`, `macos`, or `ubuntu` (linux flavor). Not guaranteed to be any of those" 42 | }, 43 | { 44 | "valueName": "platformDescription", 45 | "valueType": "String", 46 | "valueDescription": "Description of the platform, like `Windows 10 (10.0)`" 47 | } 48 | ] 49 | }, 50 | { 51 | "description": "Gets statistics about OBS, obs-websocket, and the current session.", 52 | "requestType": "GetStats", 53 | "complexity": 2, 54 | "rpcVersion": "1", 55 | "deprecated": false, 56 | "initialVersion": "5.0.0", 57 | "category": "general", 58 | "requestFields": [], 59 | "responseFields": [ 60 | { 61 | "valueName": "cpuUsage", 62 | "valueType": "Number", 63 | "valueDescription": "Current CPU usage in percent" 64 | }, 65 | { 66 | "valueName": "memoryUsage", 67 | "valueType": "Number", 68 | "valueDescription": "Amount of memory in MB currently being used by OBS" 69 | }, 70 | { 71 | "valueName": "availableDiskSpace", 72 | "valueType": "Number", 73 | "valueDescription": "Available disk space on the device being used for recording storage" 74 | }, 75 | { 76 | "valueName": "activeFps", 77 | "valueType": "Number", 78 | "valueDescription": "Current FPS being rendered" 79 | }, 80 | { 81 | "valueName": "averageFrameRenderTime", 82 | "valueType": "Number", 83 | "valueDescription": "Average time in milliseconds that OBS is taking to render a frame" 84 | }, 85 | { 86 | "valueName": "renderSkippedFrames", 87 | "valueType": "Number", 88 | "valueDescription": "Number of frames skipped by OBS in the render thread" 89 | }, 90 | { 91 | "valueName": "renderTotalFrames", 92 | "valueType": "Number", 93 | "valueDescription": "Total number of frames outputted by the render thread" 94 | }, 95 | { 96 | "valueName": "outputSkippedFrames", 97 | "valueType": "Number", 98 | "valueDescription": "Number of frames skipped by OBS in the output thread" 99 | }, 100 | { 101 | "valueName": "outputTotalFrames", 102 | "valueType": "Number", 103 | "valueDescription": "Total number of frames outputted by the output thread" 104 | }, 105 | { 106 | "valueName": "webSocketSessionIncomingMessages", 107 | "valueType": "Number", 108 | "valueDescription": "Total number of messages received by obs-websocket from the client" 109 | }, 110 | { 111 | "valueName": "webSocketSessionOutgoingMessages", 112 | "valueType": "Number", 113 | "valueDescription": "Total number of messages sent by obs-websocket to the client" 114 | } 115 | ] 116 | }, 117 | { 118 | "description": "Broadcasts a `CustomEvent` to all WebSocket clients. Receivers are clients which are identified and subscribed.", 119 | "requestType": "BroadcastCustomEvent", 120 | "complexity": 1, 121 | "rpcVersion": "1", 122 | "deprecated": false, 123 | "initialVersion": "5.0.0", 124 | "category": "general", 125 | "requestFields": [ 126 | { 127 | "valueName": "eventData", 128 | "valueType": "Object", 129 | "valueDescription": "Data payload to emit to all receivers", 130 | "valueRestrictions": null, 131 | "valueOptional": false, 132 | "valueOptionalBehavior": null 133 | } 134 | ], 135 | "responseFields": [] 136 | }, 137 | { 138 | "description": "Call a request registered to a vendor.\n\nA vendor is a unique name registered by a third-party plugin or script, which allows for custom requests and events to be added to obs-websocket.\nIf a plugin or script implements vendor requests or events, documentation is expected to be provided with them.", 139 | "requestType": "CallVendorRequest", 140 | "complexity": 3, 141 | "rpcVersion": "1", 142 | "deprecated": false, 143 | "initialVersion": "5.0.0", 144 | "category": "general", 145 | "requestFields": [ 146 | { 147 | "valueName": "vendorName", 148 | "valueType": "String", 149 | "valueDescription": "Name of the vendor to use", 150 | "valueRestrictions": null, 151 | "valueOptional": false, 152 | "valueOptionalBehavior": null 153 | }, 154 | { 155 | "valueName": "requestType", 156 | "valueType": "String", 157 | "valueDescription": "The request type to call", 158 | "valueRestrictions": null, 159 | "valueOptional": false, 160 | "valueOptionalBehavior": null 161 | }, 162 | { 163 | "valueName": "requestData", 164 | "valueType": "Object", 165 | "valueDescription": "Object containing appropriate request data", 166 | "valueRestrictions": null, 167 | "valueOptional": true, 168 | "valueOptionalBehavior": "{}" 169 | } 170 | ], 171 | "responseFields": [ 172 | { 173 | "valueName": "vendorName", 174 | "valueType": "String", 175 | "valueDescription": "Echoed of `vendorName`" 176 | }, 177 | { 178 | "valueName": "requestType", 179 | "valueType": "String", 180 | "valueDescription": "Echoed of `requestType`" 181 | }, 182 | { 183 | "valueName": "responseData", 184 | "valueType": "Object", 185 | "valueDescription": "Object containing appropriate response data. {} if request does not provide any response data" 186 | } 187 | ] 188 | }, 189 | { 190 | "description": "Gets an array of all hotkey names in OBS.\n\nNote: Hotkey functionality in obs-websocket comes as-is, and we do not guarantee support if things are broken. In 9/10 usages of hotkey requests, there exists a better, more reliable method via other requests.", 191 | "requestType": "GetHotkeyList", 192 | "complexity": 4, 193 | "rpcVersion": "1", 194 | "deprecated": false, 195 | "initialVersion": "5.0.0", 196 | "category": "general", 197 | "requestFields": [], 198 | "responseFields": [ 199 | { 200 | "valueName": "hotkeys", 201 | "valueType": "Array", 202 | "valueDescription": "Array of hotkey names" 203 | } 204 | ] 205 | }, 206 | { 207 | "description": "Triggers a hotkey using its name. See `GetHotkeyList`.\n\nNote: Hotkey functionality in obs-websocket comes as-is, and we do not guarantee support if things are broken. In 9/10 usages of hotkey requests, there exists a better, more reliable method via other requests.", 208 | "requestType": "TriggerHotkeyByName", 209 | "complexity": 4, 210 | "rpcVersion": "1", 211 | "deprecated": false, 212 | "initialVersion": "5.0.0", 213 | "category": "general", 214 | "requestFields": [ 215 | { 216 | "valueName": "hotkeyName", 217 | "valueType": "String", 218 | "valueDescription": "Name of the hotkey to trigger", 219 | "valueRestrictions": null, 220 | "valueOptional": false, 221 | "valueOptionalBehavior": null 222 | }, 223 | { 224 | "valueName": "contextName", 225 | "valueType": "String", 226 | "valueDescription": "Name of context of the hotkey to trigger", 227 | "valueRestrictions": null, 228 | "valueOptional": true, 229 | "valueOptionalBehavior": "Unknown" 230 | } 231 | ], 232 | "responseFields": [] 233 | }, 234 | { 235 | "description": "Triggers a hotkey using a sequence of keys.\n\nNote: Hotkey functionality in obs-websocket comes as-is, and we do not guarantee support if things are broken. In 9/10 usages of hotkey requests, there exists a better, more reliable method via other requests.", 236 | "requestType": "TriggerHotkeyByKeySequence", 237 | "complexity": 4, 238 | "rpcVersion": "1", 239 | "deprecated": false, 240 | "initialVersion": "5.0.0", 241 | "category": "general", 242 | "requestFields": [ 243 | { 244 | "valueName": "keyId", 245 | "valueType": "String", 246 | "valueDescription": "The OBS key ID to use. See https://github.com/obsproject/obs-studio/blob/master/libobs/obs-hotkeys.h", 247 | "valueRestrictions": null, 248 | "valueOptional": true, 249 | "valueOptionalBehavior": "Not pressed" 250 | }, 251 | { 252 | "valueName": "keyModifiers", 253 | "valueType": "Object", 254 | "valueDescription": "Object containing key modifiers to apply", 255 | "valueRestrictions": null, 256 | "valueOptional": true, 257 | "valueOptionalBehavior": "Ignored" 258 | }, 259 | { 260 | "valueName": "keyModifiers.shift", 261 | "valueType": "Boolean", 262 | "valueDescription": "Press Shift", 263 | "valueRestrictions": null, 264 | "valueOptional": true, 265 | "valueOptionalBehavior": "Not pressed" 266 | }, 267 | { 268 | "valueName": "keyModifiers.control", 269 | "valueType": "Boolean", 270 | "valueDescription": "Press CTRL", 271 | "valueRestrictions": null, 272 | "valueOptional": true, 273 | "valueOptionalBehavior": "Not pressed" 274 | }, 275 | { 276 | "valueName": "keyModifiers.alt", 277 | "valueType": "Boolean", 278 | "valueDescription": "Press ALT", 279 | "valueRestrictions": null, 280 | "valueOptional": true, 281 | "valueOptionalBehavior": "Not pressed" 282 | }, 283 | { 284 | "valueName": "keyModifiers.command", 285 | "valueType": "Boolean", 286 | "valueDescription": "Press CMD (Mac)", 287 | "valueRestrictions": null, 288 | "valueOptional": true, 289 | "valueOptionalBehavior": "Not pressed" 290 | } 291 | ], 292 | "responseFields": [] 293 | }, 294 | { 295 | "description": "Sleeps for a time duration or number of frames. Only available in request batches with types `SERIAL_REALTIME` or `SERIAL_FRAME`.", 296 | "requestType": "Sleep", 297 | "complexity": 2, 298 | "rpcVersion": "1", 299 | "deprecated": false, 300 | "initialVersion": "5.0.0", 301 | "category": "general", 302 | "requestFields": [ 303 | { 304 | "valueName": "sleepMillis", 305 | "valueType": "Number", 306 | "valueDescription": "Number of milliseconds to sleep for (if `SERIAL_REALTIME` mode)", 307 | "valueRestrictions": ">= 0, <= 50000", 308 | "valueOptional": true, 309 | "valueOptionalBehavior": "Unknown" 310 | }, 311 | { 312 | "valueName": "sleepFrames", 313 | "valueType": "Number", 314 | "valueDescription": "Number of frames to sleep for (if `SERIAL_FRAME` mode)", 315 | "valueRestrictions": ">= 0, <= 10000", 316 | "valueOptional": true, 317 | "valueOptionalBehavior": "Unknown" 318 | } 319 | ], 320 | "responseFields": [] 321 | } 322 | ] 323 | } --------------------------------------------------------------------------------