Skip to main content
Glama

OBS MCP Server

by royshil
client.ts12 kB
import WebSocket from 'ws'; import crypto from 'crypto'; import { EventEmitter } from 'events'; const logger = { log: (message: string) => console.error(message), error: (message: string) => console.error(message), debug: (message: string) => console.error(message), }; // Define OpCodes enum OpCode { Hello = 0, Identify = 1, Identified = 2, Reidentify = 3, Event = 5, Request = 6, RequestResponse = 7, RequestBatch = 8, RequestBatchResponse = 9 } // Define EventSubscription bitmasks export enum EventSubscription { None = 0, General = 1 << 0, Config = 1 << 1, Scenes = 1 << 2, Inputs = 1 << 3, Transitions = 1 << 4, Filters = 1 << 5, Outputs = 1 << 6, SceneItems = 1 << 7, MediaInputs = 1 << 8, Vendors = 1 << 9, Ui = 1 << 10, All = (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5) | (1 << 6) | (1 << 7) | (1 << 8) | (1 << 9) | (1 << 10) } // Define interfaces for message types interface BaseMessage { op: OpCode; d: any; } interface HelloMessage extends BaseMessage { d: { obsStudioVersion: string; obsWebSocketVersion: string; rpcVersion: number; authentication?: { challenge: string; salt: string; }; }; } interface IdentifyMessage extends BaseMessage { d: { rpcVersion: number; authentication?: string; eventSubscriptions: number; }; } interface IdentifiedMessage extends BaseMessage { d: { negotiatedRpcVersion: number; }; } interface RequestMessage extends BaseMessage { d: { requestType: string; requestId: string; requestData?: any; }; } interface RequestResponseMessage extends BaseMessage { d: { requestType: string; requestId: string; requestStatus: { result: boolean; code: number; comment?: string; }; responseData?: any; }; } interface EventMessage extends BaseMessage { d: { eventType: string; eventIntent: number; eventData?: any; }; } // Define the OBS WebSocket client class export class OBSWebSocketClient extends EventEmitter { private ws: WebSocket | null = null; private url: string; private password: string | null; private connected: boolean = false; private identified: boolean = false; private pendingRequests: Map<string, { resolve: Function, reject: Function, timeout: NodeJS.Timeout }> = new Map(); constructor(url: string = 'ws://localhost:4455', password: string | null = null) { super(); this.url = url; this.password = password; } /** * Connect to the OBS WebSocket server */ public async connect(): Promise<void> { if (this.connected) { return; } return new Promise<void>((resolve, reject) => { try { logger.log(`Attempting to connect to OBS WebSocket at: ${this.url}`); this.ws = new WebSocket(this.url); this.ws.on('open', () => { this.connected = true; logger.log('WebSocket connection opened successfully'); }); this.ws.on('message', (data: WebSocket.Data) => { try { const message = JSON.parse(data.toString()) as BaseMessage; this.handleMessage(message); } catch (error) { logger.error(`Error parsing message: ${error instanceof Error ? error.message : String(error)}`); } }); this.ws.on('close', (code: number, reason: Buffer) => { this.connected = false; this.identified = false; const reasonStr = reason.toString() || 'No reason provided'; logger.log(`WebSocket connection closed with code ${code}: ${reasonStr}`); this.emit('disconnected'); // Clear all pending requests this.pendingRequests.forEach((request) => { clearTimeout(request.timeout); request.reject(new Error(`WebSocket connection closed: ${reasonStr}`)); }); this.pendingRequests.clear(); }); this.ws.on('error', (error) => { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`WebSocket connection error: ${errorMessage}`); // Provide more specific error information if (errorMessage.includes('ECONNREFUSED')) { logger.error('Connection refused. Make sure OBS Studio is running and WebSocket is enabled.'); logger.error('Check that the WebSocket port (default: 4455) is not blocked by firewall.'); } else if (errorMessage.includes('ENOTFOUND')) { logger.error('Host not found. Check the OBS_WEBSOCKET_URL environment variable.'); } else if (errorMessage.includes('ETIMEDOUT')) { logger.error('Connection timed out. Check network connectivity and firewall settings.'); } reject(error); }); // Set up the identification process this.once('hello', async (hello: HelloMessage['d']) => { try { await this.identify(hello); resolve(); } catch (error) { reject(error); } }); // Set a timeout for the initial connection const connectionTimeout = setTimeout(() => { if (!this.connected) { this.ws?.terminate(); reject(new Error('WebSocket connection timeout - OBS may not be running or WebSocket may be disabled')); } }, 5000); this.ws.on('open', () => { clearTimeout(connectionTimeout); }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`Failed to create WebSocket connection: ${errorMessage}`); reject(error); } }); } /** * Check if the client is connected and identified */ public isConnected(): boolean { return this.connected && this.identified; } /** * Get connection status information */ public getConnectionStatus(): { connected: boolean; identified: boolean; url: string; hasPassword: boolean; } { return { connected: this.connected, identified: this.identified, url: this.url, hasPassword: this.password !== null }; } /** * Disconnect from the OBS WebSocket server */ public disconnect(): void { if (this.ws && this.connected) { this.ws.close(); this.ws = null; this.connected = false; this.identified = false; } } /** * Send a request to the OBS WebSocket server */ public async sendRequest<T = any>(requestType: string, requestData?: any, timeout: number = 10000): Promise<T> { if (!this.ws || !this.connected || !this.identified) { throw new Error('Not connected or identified with OBS WebSocket server'); } return new Promise<T>((resolve, reject) => { const requestId = crypto.randomUUID(); const timeoutId = setTimeout(() => { this.pendingRequests.delete(requestId); reject(new Error(`Request ${requestType} timed out after ${timeout}ms`)); }, timeout); this.pendingRequests.set(requestId, { resolve: (data: T) => { clearTimeout(timeoutId); resolve(data); }, reject: (error: Error) => { clearTimeout(timeoutId); reject(error); }, timeout: timeoutId }); const requestMessage: RequestMessage = { op: OpCode.Request, d: { requestType, requestId, requestData } }; this.ws!.send(JSON.stringify(requestMessage)); }); } /** * Handle incoming messages from the OBS WebSocket server */ private handleMessage(message: BaseMessage): void { switch (message.op) { case OpCode.Hello: this.emit('hello', (message as HelloMessage).d); break; case OpCode.Identified: this.identified = true; this.emit('identified', (message as IdentifiedMessage).d); break; case OpCode.RequestResponse: this.handleRequestResponse(message as RequestResponseMessage); break; case OpCode.Event: this.handleEvent(message as EventMessage); break; default: logger.debug(`Unhandled message type: ${message.op}`); break; } } /** * Handle request responses from the OBS WebSocket server */ private handleRequestResponse(message: RequestResponseMessage): void { const { requestId, requestStatus, responseData } = message.d; const pendingRequest = this.pendingRequests.get(requestId); if (pendingRequest) { this.pendingRequests.delete(requestId); if (requestStatus.result) { pendingRequest.resolve(responseData || {}); } else { const errorMessage = `Request failed: ${requestStatus.code} ${requestStatus.comment || ''}`; pendingRequest.reject(new Error(errorMessage)); } } } /** * Handle events from the OBS WebSocket server */ private handleEvent(message: EventMessage): void { const { eventType, eventData } = message.d; this.emit('event', eventType, eventData); this.emit(eventType, eventData); } /** * Identify with the OBS WebSocket server */ private async identify(hello: HelloMessage['d']): Promise<void> { if (!this.ws || !this.connected) { throw new Error('Not connected to OBS WebSocket server'); } logger.log(`Received hello from OBS WebSocket v${hello.obsWebSocketVersion} (OBS v${hello.obsStudioVersion})`); logger.log(`RPC Version: ${hello.rpcVersion}`); let authentication: string | undefined; // Handle authentication if required if (hello.authentication && this.password) { logger.log('Authentication required, generating auth string...'); authentication = this.generateAuthenticationString( this.password, hello.authentication.salt, hello.authentication.challenge ); } else if (hello.authentication && !this.password) { const errorMsg = 'Password required for authentication but not provided. Set OBS_WEBSOCKET_PASSWORD environment variable.'; logger.error(errorMsg); throw new Error(errorMsg); } else if (!hello.authentication) { logger.log('No authentication required'); } const identifyMessage: IdentifyMessage = { op: OpCode.Identify, d: { rpcVersion: hello.rpcVersion, eventSubscriptions: EventSubscription.All, } }; if (authentication) { identifyMessage.d.authentication = authentication; } return new Promise<void>((resolve, reject) => { // Set up a one-time listener for the Identified message this.once('identified', () => { logger.log('Successfully identified with OBS WebSocket server'); resolve(); }); // Set a timeout for identification const timeoutId = setTimeout(() => { const errorMsg = 'Identification timed out - OBS may be unresponsive or authentication failed'; logger.error(errorMsg); reject(new Error(errorMsg)); }, 5000); this.once('identified', () => clearTimeout(timeoutId)); // Send the Identify message logger.log('Sending identify message...'); this.ws!.send(JSON.stringify(identifyMessage)); }); } /** * Generate authentication string for OBS WebSocket */ private generateAuthenticationString(password: string, salt: string, challenge: string): string { // Create SHA256 Base64 encoded secret const secretBytes = crypto.createHash('sha256') .update(password + salt) .digest(); const secret = secretBytes.toString('base64'); // Create authentication string const authBytes = crypto.createHash('sha256') .update(secret + challenge) .digest(); const authentication = authBytes.toString('base64'); return authentication; } } export default OBSWebSocketClient;

MCP directory API

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

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

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