From 1d343accaa506ad115c6a74f445530c3b9bc43dd Mon Sep 17 00:00:00 2001 From: Tom Main Date: Tue, 22 Sep 2026 17:50:02 -0400 Subject: [PATCH 1/3] Declare single-request tools instead of hand-writing their handlers 90 tools that send one OBS request and either print its JSON or a fixed confirmation now use registerObsRequestTool, with an optional successMessage built from the arguments. Names, titles, input schemas, and annotations are unchanged (the tool contract snapshot is identical). Read-only getters that formatted the response as prose, such as obs-get-current-scene and obs-get-input-mute, now return the OBS response as JSON so structuredContent carries the real fields. Error text is now ' failed: <reason>'. 34 tools with custom behavior stay hand-written. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vjn8uBEa4mJshaQU3pFuqX --- src/tools/config.ts | 649 +++++++++----------------------------- src/tools/filters.ts | 379 ++++++---------------- src/tools/general.ts | 160 ++-------- src/tools/inputs.ts | 627 +++++++++--------------------------- src/tools/media-inputs.ts | 200 ++++-------- src/tools/outputs.ts | 495 +++++++---------------------- src/tools/record.ts | 195 +++--------- src/tools/request-tool.ts | 11 +- src/tools/scene-items.ts | 213 ++++--------- src/tools/scenes.ts | 344 +++++--------------- src/tools/sources.ts | 123 ++------ src/tools/streaming.ts | 123 ++------ src/tools/transitions.ts | 320 ++++--------------- src/tools/ui.ts | 122 ++----- 14 files changed, 921 insertions(+), 3040 deletions(-) diff --git a/src/tools/config.ts b/src/tools/config.ts index d5798b0..bed3ab9 100644 --- a/src/tools/config.ts +++ b/src/tools/config.ts @@ -6,367 +6,134 @@ import { McpServer } from "@modelcontextprotocol/server"; import { OBSWebSocketClient } from "../client.js"; import { z } from "zod"; +import { registerObsRequestTool } from "./request-tool.js"; const OUTPUT_PARAMETER_CATEGORIES = new Set(["Output", "SimpleOutput", "AdvOut"]); export function initialize(server: McpServer, client: OBSWebSocketClient): void { // GetPersistentData tool - server.registerTool( - "obs-get-persistent-data", - { - title: "Get Persistent Data", - description: "Gets the value of a slot from the selected persistent data realm", - inputSchema: z.object({ - realm: z.string().describe("The data realm to select. OBS_WEBSOCKET_DATA_REALM_GLOBAL or OBS_WEBSOCKET_DATA_REALM_PROFILE"), - slotName: z.string().describe("The name of the slot to retrieve data from") - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ realm, slotName }) => { - try { - const response = await client.sendRequest("GetPersistentData", { realm, slotName }); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting persistent data: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-persistent-data", + title: "Get Persistent Data", + description: "Gets the value of a slot from the selected persistent data realm", + requestType: "GetPersistentData", + inputSchema: z.object({ + realm: z.string().describe("The data realm to select. OBS_WEBSOCKET_DATA_REALM_GLOBAL or OBS_WEBSOCKET_DATA_REALM_PROFILE"), + slotName: z.string().describe("The name of the slot to retrieve data from") + }), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetPersistentData tool - server.registerTool( - "obs-set-persistent-data", - { - title: "Set Persistent Data", - description: "Sets the value of a slot from the selected persistent data realm", - inputSchema: z.object({ - realm: z.string().describe("The data realm to select. OBS_WEBSOCKET_DATA_REALM_GLOBAL or OBS_WEBSOCKET_DATA_REALM_PROFILE"), - slotName: z.string().describe("The name of the slot to set data for"), - slotValue: z.unknown().describe("The value to apply to the slot") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ realm, slotName, slotValue }) => { - try { - await client.sendRequest("SetPersistentData", { realm, slotName, slotValue }); - return { - content: [ - { - type: "text", - text: `Successfully set value for slot '${slotName}' in realm '${realm}'` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error setting persistent data: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-set-persistent-data", + title: "Set Persistent Data", + description: "Sets the value of a slot from the selected persistent data realm", + requestType: "SetPersistentData", + inputSchema: z.object({ + realm: z.string().describe("The data realm to select. OBS_WEBSOCKET_DATA_REALM_GLOBAL or OBS_WEBSOCKET_DATA_REALM_PROFILE"), + slotName: z.string().describe("The name of the slot to set data for"), + slotValue: z.unknown().describe("The value to apply to the slot") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: ({ realm, slotName }) => `Successfully set value for slot '${slotName}' in realm '${realm}'`, + }); // GetSceneCollectionList tool - server.registerTool( - "obs-get-scene-collection-list", - { - title: "Get Scene Collection List", - description: "Gets an array of all scene collections", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const response = await client.sendRequest("GetSceneCollectionList"); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting scene collection list: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-scene-collection-list", + title: "Get Scene Collection List", + description: "Gets an array of all scene collections", + requestType: "GetSceneCollectionList", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetCurrentSceneCollection tool - server.registerTool( - "obs-set-current-scene-collection", - { - title: "Set Current Scene Collection", - description: "Switches to a scene collection", - inputSchema: z.object({ - sceneCollectionName: z.string().describe("Name of the scene collection to switch to") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ sceneCollectionName }) => { - try { - await client.sendRequest("SetCurrentSceneCollection", { sceneCollectionName }); - return { - content: [ - { - type: "text", - text: `Successfully switched to scene collection: ${sceneCollectionName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error switching scene collection: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-set-current-scene-collection", + title: "Set Current Scene Collection", + description: "Switches to a scene collection", + requestType: "SetCurrentSceneCollection", + inputSchema: z.object({ + sceneCollectionName: z.string().describe("Name of the scene collection to switch to") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: ({ sceneCollectionName }) => `Successfully switched to scene collection: ${sceneCollectionName}`, + }); // CreateSceneCollection tool - server.registerTool( - "obs-create-scene-collection", - { - title: "Create Scene Collection", - description: "Creates a new scene collection, switching to it in the process", - inputSchema: z.object({ - sceneCollectionName: z.string().describe("Name for the new scene collection") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, - }, - async ({ sceneCollectionName }) => { - try { - await client.sendRequest("CreateSceneCollection", { sceneCollectionName }); - return { - content: [ - { - type: "text", - text: `Successfully created and switched to scene collection: ${sceneCollectionName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error creating scene collection: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-create-scene-collection", + title: "Create Scene Collection", + description: "Creates a new scene collection, switching to it in the process", + requestType: "CreateSceneCollection", + inputSchema: z.object({ + sceneCollectionName: z.string().describe("Name for the new scene collection") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + successMessage: ({ sceneCollectionName }) => `Successfully created and switched to scene collection: ${sceneCollectionName}`, + }); // GetProfileList tool - server.registerTool( - "obs-get-profile-list", - { - title: "Get Profile List", - description: "Gets an array of all profiles", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const response = await client.sendRequest("GetProfileList"); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting profile list: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-profile-list", + title: "Get Profile List", + description: "Gets an array of all profiles", + requestType: "GetProfileList", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetCurrentProfile tool - server.registerTool( - "obs-set-current-profile", - { - title: "Set Current Profile", - description: "Switches to a profile", - inputSchema: z.object({ - profileName: z.string().describe("Name of the profile to switch to") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ profileName }) => { - try { - await client.sendRequest("SetCurrentProfile", { profileName }); - return { - content: [ - { - type: "text", - text: `Successfully switched to profile: ${profileName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error switching profile: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-set-current-profile", + title: "Set Current Profile", + description: "Switches to a profile", + requestType: "SetCurrentProfile", + inputSchema: z.object({ + profileName: z.string().describe("Name of the profile to switch to") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: ({ profileName }) => `Successfully switched to profile: ${profileName}`, + }); // CreateProfile tool - server.registerTool( - "obs-create-profile", - { - title: "Create Profile", - description: "Creates a new profile, switching to it in the process", - inputSchema: z.object({ - profileName: z.string().describe("Name for the new profile") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, - }, - async ({ profileName }) => { - try { - await client.sendRequest("CreateProfile", { profileName }); - return { - content: [ - { - type: "text", - text: `Successfully created and switched to profile: ${profileName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error creating profile: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-create-profile", + title: "Create Profile", + description: "Creates a new profile, switching to it in the process", + requestType: "CreateProfile", + inputSchema: z.object({ + profileName: z.string().describe("Name for the new profile") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + successMessage: ({ profileName }) => `Successfully created and switched to profile: ${profileName}`, + }); // RemoveProfile tool - server.registerTool( - "obs-remove-profile", - { - title: "Remove Profile", - description: "Removes a profile. If the current profile is chosen, it will change to a different profile first", - inputSchema: z.object({ - profileName: z.string().describe("Name of the profile to remove") - }), - annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }, - }, - async ({ profileName }) => { - try { - await client.sendRequest("RemoveProfile", { profileName }); - return { - content: [ - { - type: "text", - text: `Successfully removed profile: ${profileName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error removing profile: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-remove-profile", + title: "Remove Profile", + description: "Removes a profile. If the current profile is chosen, it will change to a different profile first", + requestType: "RemoveProfile", + inputSchema: z.object({ + profileName: z.string().describe("Name of the profile to remove") + }), + annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }, + successMessage: ({ profileName }) => `Successfully removed profile: ${profileName}`, + }); // GetProfileParameter tool - server.registerTool( - "obs-get-profile-parameter", - { - title: "Get Profile Parameter", - description: "Gets a parameter from the current profile's configuration", - inputSchema: z.object({ - parameterCategory: z.string().describe("Category of the parameter to get"), - parameterName: z.string().describe("Name of the parameter to get") - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ parameterCategory, parameterName }) => { - try { - const response = await client.sendRequest("GetProfileParameter", { parameterCategory, parameterName }); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting profile parameter: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-profile-parameter", + title: "Get Profile Parameter", + description: "Gets a parameter from the current profile's configuration", + requestType: "GetProfileParameter", + inputSchema: z.object({ + parameterCategory: z.string().describe("Category of the parameter to get"), + parameterName: z.string().describe("Name of the parameter to get") + }), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetProfileParameter tool server.registerTool( @@ -411,37 +178,13 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // GetVideoSettings tool - server.registerTool( - "obs-get-video-settings", - { - title: "Get Video Settings", - description: "Gets the current video settings", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const response = await client.sendRequest("GetVideoSettings"); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting video settings: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-video-settings", + title: "Get Video Settings", + description: "Gets the current video settings", + requestType: "GetVideoSettings", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetVideoSettings tool server.registerTool( @@ -494,141 +237,47 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // GetStreamServiceSettings tool - server.registerTool( - "obs-get-stream-service-settings", - { - title: "Get Stream Service Settings", - description: "Gets the current stream service settings", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const response = await client.sendRequest("GetStreamServiceSettings"); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting stream service settings: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-stream-service-settings", + title: "Get Stream Service Settings", + description: "Gets the current stream service settings", + requestType: "GetStreamServiceSettings", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetStreamServiceSettings tool - server.registerTool( - "obs-set-stream-service-settings", - { - title: "Set Stream Service Settings", - description: "Sets the current stream service settings", - inputSchema: z.object({ - streamServiceType: z.string().describe("Type of stream service to apply. Example: rtmp_common or rtmp_custom"), - streamServiceSettings: z.record(z.string(), z.unknown()).describe("Settings to apply to the service") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ streamServiceType, streamServiceSettings }) => { - try { - await client.sendRequest("SetStreamServiceSettings", { streamServiceType, streamServiceSettings }); - return { - content: [ - { - type: "text", - text: `Successfully updated stream service settings to type: ${streamServiceType}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error setting stream service settings: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-set-stream-service-settings", + title: "Set Stream Service Settings", + description: "Sets the current stream service settings", + requestType: "SetStreamServiceSettings", + inputSchema: z.object({ + streamServiceType: z.string().describe("Type of stream service to apply. Example: rtmp_common or rtmp_custom"), + streamServiceSettings: z.record(z.string(), z.unknown()).describe("Settings to apply to the service") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: ({ streamServiceType }) => `Successfully updated stream service settings to type: ${streamServiceType}`, + }); // GetRecordDirectory tool - server.registerTool( - "obs-get-record-directory", - { - title: "Get Record Directory", - description: "Gets the current directory that the record output is set to", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const response = await client.sendRequest("GetRecordDirectory"); - return { - content: [ - { - type: "text", - text: response.recordDirectory - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting record directory: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-record-directory", + title: "Get Record Directory", + description: "Gets the current directory that the record output is set to", + requestType: "GetRecordDirectory", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetRecordDirectory tool - server.registerTool( - "obs-set-record-directory", - { - title: "Set Record Directory", - description: "Sets the current directory that the record output writes files to", - inputSchema: z.object({ - recordDirectory: z.string().describe("Output directory") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ recordDirectory }) => { - try { - await client.sendRequest("SetRecordDirectory", { recordDirectory }); - return { - content: [ - { - type: "text", - text: `Successfully set record directory to: ${recordDirectory}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error setting record directory: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-set-record-directory", + title: "Set Record Directory", + description: "Sets the current directory that the record output writes files to", + requestType: "SetRecordDirectory", + inputSchema: z.object({ + recordDirectory: z.string().describe("Output directory") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: ({ recordDirectory }) => `Successfully set record directory to: ${recordDirectory}`, + }); } diff --git a/src/tools/filters.ts b/src/tools/filters.ts index 95c51f1..924fe27 100644 --- a/src/tools/filters.ts +++ b/src/tools/filters.ts @@ -6,113 +6,42 @@ import { McpServer } from "@modelcontextprotocol/server"; import { OBSWebSocketClient } from "../client.js"; import { z } from "zod"; +import { registerObsRequestTool } from "./request-tool.js"; export function initialize(server: McpServer, client: OBSWebSocketClient): void { // GetSourceFilterKindList tool - server.registerTool( - "obs-get-filter-kind-list", - { - title: "Get Filter Kind List", - description: "Gets an array of all available source filter kinds", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const response = await client.sendRequest("GetSourceFilterKindList"); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting filter kind list: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-filter-kind-list", + title: "Get Filter Kind List", + description: "Gets an array of all available source filter kinds", + requestType: "GetSourceFilterKindList", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // GetSourceFilterList tool - server.registerTool( - "obs-get-source-filter-list", - { - title: "Get Source Filter List", - description: "Gets an array of all of a source's filters", - inputSchema: z.object({ - canvasUuid: z.string().optional().describe("UUID of the canvas containing the source"), - sourceName: z.string().describe("Name of the source") - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ canvasUuid, sourceName }) => { - try { - const response = await client.sendRequest("GetSourceFilterList", { canvasUuid, sourceName }); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting source filter list: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-source-filter-list", + title: "Get Source Filter List", + description: "Gets an array of all of a source's filters", + requestType: "GetSourceFilterList", + inputSchema: z.object({ + canvasUuid: z.string().optional().describe("UUID of the canvas containing the source"), + sourceName: z.string().describe("Name of the source") + }), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // GetSourceFilterDefaultSettings tool - server.registerTool( - "obs-get-filter-default-settings", - { - title: "Get Filter Default Settings", - description: "Gets the default settings for a filter kind", - inputSchema: z.object({ - filterKind: z.string().describe("Filter kind to get the default settings for") - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ filterKind }) => { - try { - const response = await client.sendRequest("GetSourceFilterDefaultSettings", { filterKind }); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting filter default settings: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-filter-default-settings", + title: "Get Filter Default Settings", + description: "Gets the default settings for a filter kind", + requestType: "GetSourceFilterDefaultSettings", + inputSchema: z.object({ + filterKind: z.string().describe("Filter kind to get the default settings for") + }), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // CreateSourceFilter tool server.registerTool( @@ -160,158 +89,65 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // RemoveSourceFilter tool - server.registerTool( - "obs-remove-source-filter", - { - title: "Remove Source Filter", - description: "Removes a filter from a source", - inputSchema: z.object({ - canvasUuid: z.string().optional().describe("UUID of the canvas containing the source"), - sourceName: z.string().describe("Name of the source the filter is on"), - filterName: z.string().describe("Name of the filter to remove") - }), - annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }, - }, - async ({ canvasUuid, sourceName, filterName }) => { - try { - await client.sendRequest("RemoveSourceFilter", { canvasUuid, sourceName, filterName }); - return { - content: [ - { - type: "text", - text: `Successfully removed filter '${filterName}' from source '${sourceName}'` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error removing source filter: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-remove-source-filter", + title: "Remove Source Filter", + description: "Removes a filter from a source", + requestType: "RemoveSourceFilter", + inputSchema: z.object({ + canvasUuid: z.string().optional().describe("UUID of the canvas containing the source"), + sourceName: z.string().describe("Name of the source the filter is on"), + filterName: z.string().describe("Name of the filter to remove") + }), + annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }, + successMessage: ({ filterName, sourceName }) => `Successfully removed filter '${filterName}' from source '${sourceName}'`, + }); // SetSourceFilterName tool - server.registerTool( - "obs-set-source-filter-name", - { - title: "Rename Source Filter", - description: "Sets the name of a source filter (rename)", - inputSchema: z.object({ - canvasUuid: z.string().optional().describe("UUID of the canvas containing the source"), - sourceName: z.string().describe("Name of the source the filter is on"), - filterName: z.string().describe("Current name of the filter"), - newFilterName: z.string().describe("New name for the filter") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ canvasUuid, sourceName, filterName, newFilterName }) => { - try { - await client.sendRequest("SetSourceFilterName", { canvasUuid, sourceName, filterName, newFilterName }); - return { - content: [ - { - type: "text", - text: `Successfully renamed filter '${filterName}' to '${newFilterName}' on source '${sourceName}'` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error renaming source filter: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-set-source-filter-name", + title: "Rename Source Filter", + description: "Sets the name of a source filter (rename)", + requestType: "SetSourceFilterName", + inputSchema: z.object({ + canvasUuid: z.string().optional().describe("UUID of the canvas containing the source"), + sourceName: z.string().describe("Name of the source the filter is on"), + filterName: z.string().describe("Current name of the filter"), + newFilterName: z.string().describe("New name for the filter") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: ({ filterName, newFilterName, sourceName }) => `Successfully renamed filter '${filterName}' to '${newFilterName}' on source '${sourceName}'`, + }); // GetSourceFilter tool - server.registerTool( - "obs-get-source-filter", - { - title: "Get Source Filter", - description: "Gets the info for a specific source filter", - inputSchema: z.object({ - canvasUuid: z.string().optional().describe("UUID of the canvas containing the source"), - sourceName: z.string().describe("Name of the source"), - filterName: z.string().describe("Name of the filter") - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ canvasUuid, sourceName, filterName }) => { - try { - const response = await client.sendRequest("GetSourceFilter", { canvasUuid, sourceName, filterName }); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting source filter info: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-source-filter", + title: "Get Source Filter", + description: "Gets the info for a specific source filter", + requestType: "GetSourceFilter", + inputSchema: z.object({ + canvasUuid: z.string().optional().describe("UUID of the canvas containing the source"), + sourceName: z.string().describe("Name of the source"), + filterName: z.string().describe("Name of the filter") + }), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetSourceFilterIndex tool - server.registerTool( - "obs-set-source-filter-index", - { - title: "Set Source Filter Index", - description: "Sets the index position of a filter on a source", - inputSchema: z.object({ - canvasUuid: z.string().optional().describe("UUID of the canvas containing the source"), - sourceName: z.string().describe("Name of the source the filter is on"), - filterName: z.string().describe("Name of the filter"), - filterIndex: z.number().min(0).describe("New index position of the filter") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ canvasUuid, sourceName, filterName, filterIndex }) => { - try { - await client.sendRequest("SetSourceFilterIndex", { canvasUuid, sourceName, filterName, filterIndex }); - return { - content: [ - { - type: "text", - text: `Successfully set filter '${filterName}' to index ${filterIndex} on source '${sourceName}'` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error setting source filter index: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-set-source-filter-index", + title: "Set Source Filter Index", + description: "Sets the index position of a filter on a source", + requestType: "SetSourceFilterIndex", + inputSchema: z.object({ + canvasUuid: z.string().optional().describe("UUID of the canvas containing the source"), + sourceName: z.string().describe("Name of the source the filter is on"), + filterName: z.string().describe("Name of the filter"), + filterIndex: z.number().min(0).describe("New index position of the filter") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: ({ filterIndex, filterName, sourceName }) => `Successfully set filter '${filterName}' to index ${filterIndex} on source '${sourceName}'`, + }); // SetSourceFilterSettings tool server.registerTool( @@ -359,41 +195,18 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // SetSourceFilterEnabled tool - server.registerTool( - "obs-set-source-filter-enabled", - { - title: "Set Source Filter Enabled", - description: "Sets the enable state of a source filter", - inputSchema: z.object({ - canvasUuid: z.string().optional().describe("UUID of the canvas containing the source"), - sourceName: z.string().describe("Name of the source the filter is on"), - filterName: z.string().describe("Name of the filter"), - filterEnabled: z.boolean().describe("New enable state of the filter") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ canvasUuid, sourceName, filterName, filterEnabled }) => { - try { - await client.sendRequest("SetSourceFilterEnabled", { canvasUuid, sourceName, filterName, filterEnabled }); - return { - content: [ - { - type: "text", - text: `Successfully ${filterEnabled ? "enabled" : "disabled"} filter '${filterName}' on source '${sourceName}'` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error setting source filter enabled state: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-set-source-filter-enabled", + title: "Set Source Filter Enabled", + description: "Sets the enable state of a source filter", + requestType: "SetSourceFilterEnabled", + inputSchema: z.object({ + canvasUuid: z.string().optional().describe("UUID of the canvas containing the source"), + sourceName: z.string().describe("Name of the source the filter is on"), + filterName: z.string().describe("Name of the filter"), + filterEnabled: z.boolean().describe("New enable state of the filter") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: ({ filterEnabled, filterName, sourceName }) => `Successfully ${filterEnabled ? "enabled" : "disabled"} filter '${filterName}' on source '${sourceName}'`, + }); } diff --git a/src/tools/general.ts b/src/tools/general.ts index 66a7c92..a0adb54 100644 --- a/src/tools/general.ts +++ b/src/tools/general.ts @@ -7,6 +7,7 @@ import { McpServer } from "@modelcontextprotocol/server"; import { OBSWebSocketClient } from "../client.js"; import { PACKAGE_VERSION } from "../version.js"; import { z } from "zod"; +import { registerObsRequestTool } from "./request-tool.js"; const MAX_SLEEP_MILLIS = 50_000; @@ -50,37 +51,13 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // Get OBS version info - server.registerTool( - "obs-get-version", - { - title: "OBS Version Info", - description: "Get OBS Studio version information", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const version = await client.sendRequest("GetVersion"); - return { - content: [ - { - type: "text", - text: JSON.stringify(version, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Failed to get OBS version: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-version", + title: "OBS Version Info", + description: "Get OBS Studio version information", + requestType: "GetVersion", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // Test OBS connection server.registerTool( @@ -117,73 +94,26 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // GetStats tool - server.registerTool( - "obs-get-stats", - { - title: "OBS Statistics", - description: "Gets statistics about OBS, obs-websocket, and the current session", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const stats = await client.sendRequest("GetStats"); - return { - content: [ - { - type: "text", - text: JSON.stringify(stats, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting stats: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-stats", + title: "OBS Statistics", + description: "Gets statistics about OBS, obs-websocket, and the current session", + requestType: "GetStats", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // BroadcastCustomEvent tool - server.registerTool( - "obs-broadcast-custom-event", - { - title: "Broadcast Custom Event", - description: "Broadcasts a CustomEvent to all WebSocket clients", - inputSchema: z.object({ - eventData: z.record(z.string(), z.unknown()).describe("Data payload to emit to all receivers") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, - }, - async ({ eventData }) => { - try { - await client.sendRequest("BroadcastCustomEvent", { eventData }); - return { - content: [ - { - type: "text", - text: "Custom event broadcast successfully" - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error broadcasting custom event: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-broadcast-custom-event", + title: "Broadcast Custom Event", + description: "Broadcasts a CustomEvent to all WebSocket clients", + requestType: "BroadcastCustomEvent", + inputSchema: z.object({ + eventData: z.record(z.string(), z.unknown()).describe("Data payload to emit to all receivers") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + successMessage: () => "Custom event broadcast successfully", + }); // CallVendorRequest tool server.registerTool( @@ -233,37 +163,13 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // GetHotkeyList tool - server.registerTool( - "obs-get-hotkey-list", - { - title: "Get Hotkey List", - description: "Gets an array of all hotkey names in OBS", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const hotkeyList = await client.sendRequest("GetHotkeyList"); - return { - content: [ - { - type: "text", - text: JSON.stringify(hotkeyList, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting hotkey list: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-hotkey-list", + title: "Get Hotkey List", + description: "Gets an array of all hotkey names in OBS", + requestType: "GetHotkeyList", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // TriggerHotkeyByName tool server.registerTool( diff --git a/src/tools/inputs.ts b/src/tools/inputs.ts index 6385438..92924b5 100644 --- a/src/tools/inputs.ts +++ b/src/tools/inputs.ts @@ -6,6 +6,7 @@ import { McpServer } from "@modelcontextprotocol/server"; import { OBSWebSocketClient } from "../client.js"; import { z } from "zod"; +import { registerObsRequestTool } from "./request-tool.js"; export function initialize(server: McpServer, client: OBSWebSocketClient): void { // GetInputList tool @@ -91,37 +92,13 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // GetSpecialInputs tool - server.registerTool( - "obs-get-special-inputs", - { - title: "Get Special Inputs", - description: "Gets the names of all special inputs", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const response = await client.sendRequest("GetSpecialInputs"); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting special inputs: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-special-inputs", + title: "Get Special Inputs", + description: "Gets the names of all special inputs", + requestType: "GetSpecialInputs", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // CreateInput tool server.registerTool( @@ -173,149 +150,55 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // RemoveInput tool - server.registerTool( - "obs-remove-input", - { - title: "Remove Input", - description: "Removes an existing input", - inputSchema: z.object({ - inputName: z.string().describe("Name of the input to remove") - }), - annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }, - }, - async ({ inputName }) => { - try { - await client.sendRequest("RemoveInput", { inputName }); - return { - content: [ - { - type: "text", - text: `Successfully removed input: ${inputName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error removing input: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-remove-input", + title: "Remove Input", + description: "Removes an existing input", + requestType: "RemoveInput", + inputSchema: z.object({ + inputName: z.string().describe("Name of the input to remove") + }), + annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }, + successMessage: ({ inputName }) => `Successfully removed input: ${inputName}`, + }); // SetInputName tool - server.registerTool( - "obs-set-input-name", - { - title: "Rename Input", - description: "Sets the name of an input (rename)", - inputSchema: z.object({ - inputName: z.string().describe("Current input name"), - newInputName: z.string().describe("New name for the input") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ inputName, newInputName }) => { - try { - await client.sendRequest("SetInputName", { inputName, newInputName }); - return { - content: [ - { - type: "text", - text: `Successfully renamed input '${inputName}' to '${newInputName}'` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error renaming input: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-set-input-name", + title: "Rename Input", + description: "Sets the name of an input (rename)", + requestType: "SetInputName", + inputSchema: z.object({ + inputName: z.string().describe("Current input name"), + newInputName: z.string().describe("New name for the input") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: ({ inputName, newInputName }) => `Successfully renamed input '${inputName}' to '${newInputName}'`, + }); // GetInputDefaultSettings tool - server.registerTool( - "obs-get-input-default-settings", - { - title: "Get Input Default Settings", - description: "Gets the default settings for an input kind", - inputSchema: z.object({ - inputKind: z.string().describe("Input kind to get the default settings for") - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ inputKind }) => { - try { - const response = await client.sendRequest("GetInputDefaultSettings", { inputKind }); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting input default settings: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-input-default-settings", + title: "Get Input Default Settings", + description: "Gets the default settings for an input kind", + requestType: "GetInputDefaultSettings", + inputSchema: z.object({ + inputKind: z.string().describe("Input kind to get the default settings for") + }), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // GetInputSettings tool - server.registerTool( - "obs-get-input-settings", - { - title: "Get Input Settings", - description: "Gets the settings of an input", - inputSchema: z.object({ - inputName: z.string().describe("Name of the input to get the settings of") - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ inputName }) => { - try { - const response = await client.sendRequest("GetInputSettings", { inputName }); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting input settings: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-input-settings", + title: "Get Input Settings", + description: "Gets the settings of an input", + requestType: "GetInputSettings", + inputSchema: z.object({ + inputName: z.string().describe("Name of the input to get the settings of") + }), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetInputSettings tool server.registerTool( @@ -361,77 +244,30 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // GetInputMute tool - server.registerTool( - "obs-get-input-mute", - { - title: "Get Input Mute State", - description: "Gets the audio mute state of an input", - inputSchema: z.object({ - inputName: z.string().describe("Name of input to get the mute state of") - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ inputName }) => { - try { - const response = await client.sendRequest("GetInputMute", { inputName }); - return { - content: [ - { - type: "text", - text: `Input '${inputName}' is ${response.inputMuted ? "muted" : "unmuted"}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting input mute state: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-input-mute", + title: "Get Input Mute State", + description: "Gets the audio mute state of an input", + requestType: "GetInputMute", + inputSchema: z.object({ + inputName: z.string().describe("Name of input to get the mute state of") + }), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetInputMute tool - server.registerTool( - "obs-set-input-mute", - { - title: "Set Input Mute", - description: "Sets the audio mute state of an input", - inputSchema: z.object({ - inputName: z.string().describe("Name of the input to set the mute state of"), - inputMuted: z.boolean().describe("Whether to mute the input or not") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ inputName, inputMuted }) => { - try { - await client.sendRequest("SetInputMute", { inputName, inputMuted }); - return { - content: [ - { - type: "text", - text: `Successfully ${inputMuted ? "muted" : "unmuted"} input: ${inputName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error setting input mute state: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-set-input-mute", + title: "Set Input Mute", + description: "Sets the audio mute state of an input", + requestType: "SetInputMute", + inputSchema: z.object({ + inputName: z.string().describe("Name of the input to set the mute state of"), + inputMuted: z.boolean().describe("Whether to mute the input or not") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: ({ inputMuted, inputName }) => `Successfully ${inputMuted ? "muted" : "unmuted"} input: ${inputName}`, + }); // ToggleInputMute tool server.registerTool( @@ -470,40 +306,16 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // GetInputVolume tool - server.registerTool( - "obs-get-input-volume", - { - title: "Get Input Volume", - description: "Gets the current volume setting of an input", - inputSchema: z.object({ - inputName: z.string().describe("Name of the input to get the volume of") - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ inputName }) => { - try { - const response = await client.sendRequest("GetInputVolume", { inputName }); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting input volume: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-input-volume", + title: "Get Input Volume", + description: "Gets the current volume setting of an input", + requestType: "GetInputVolume", + inputSchema: z.object({ + inputName: z.string().describe("Name of the input to get the volume of") + }), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetInputVolume tool server.registerTool( @@ -556,221 +368,80 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // GetInputAudioBalance tool - server.registerTool( - "obs-get-input-audio-balance", - { - title: "Get Input Audio Balance", - description: "Gets the audio balance of an input", - inputSchema: z.object({ - inputName: z.string().describe("Name of the input to get the audio balance of") - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ inputName }) => { - try { - const response = await client.sendRequest("GetInputAudioBalance", { inputName }); - return { - content: [ - { - type: "text", - text: `Audio balance for input '${inputName}': ${response.inputAudioBalance}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting input audio balance: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-input-audio-balance", + title: "Get Input Audio Balance", + description: "Gets the audio balance of an input", + requestType: "GetInputAudioBalance", + inputSchema: z.object({ + inputName: z.string().describe("Name of the input to get the audio balance of") + }), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetInputAudioBalance tool - server.registerTool( - "obs-set-input-audio-balance", - { - title: "Set Input Audio Balance", - description: "Sets the audio balance of an input", - inputSchema: z.object({ - inputName: z.string().describe("Name of the input to set the audio balance of"), - inputAudioBalance: z.number().min(0).max(1).describe("New audio balance value (0.0-1.0)") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ inputName, inputAudioBalance }) => { - try { - await client.sendRequest("SetInputAudioBalance", { inputName, inputAudioBalance }); - return { - content: [ - { - type: "text", - text: `Successfully set audio balance to ${inputAudioBalance} for input: ${inputName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error setting input audio balance: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-set-input-audio-balance", + title: "Set Input Audio Balance", + description: "Sets the audio balance of an input", + requestType: "SetInputAudioBalance", + inputSchema: z.object({ + inputName: z.string().describe("Name of the input to set the audio balance of"), + inputAudioBalance: z.number().min(0).max(1).describe("New audio balance value (0.0-1.0)") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: ({ inputAudioBalance, inputName }) => `Successfully set audio balance to ${inputAudioBalance} for input: ${inputName}`, + }); // GetInputAudioSyncOffset tool - server.registerTool( - "obs-get-input-audio-sync-offset", - { - title: "Get Input Audio Sync Offset", - description: "Gets the audio sync offset of an input", - inputSchema: z.object({ - inputName: z.string().describe("Name of the input to get the audio sync offset of") - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ inputName }) => { - try { - const response = await client.sendRequest("GetInputAudioSyncOffset", { inputName }); - return { - content: [ - { - type: "text", - text: `Audio sync offset for input '${inputName}': ${response.inputAudioSyncOffset}ms` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting input audio sync offset: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-input-audio-sync-offset", + title: "Get Input Audio Sync Offset", + description: "Gets the audio sync offset of an input", + requestType: "GetInputAudioSyncOffset", + inputSchema: z.object({ + inputName: z.string().describe("Name of the input to get the audio sync offset of") + }), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetInputAudioSyncOffset tool - server.registerTool( - "obs-set-input-audio-sync-offset", - { - title: "Set Input Audio Sync Offset", - description: "Sets the audio sync offset of an input", - inputSchema: z.object({ - inputName: z.string().describe("Name of the input to set the audio sync offset of"), - inputAudioSyncOffset: z.number().min(-950).max(20000).describe("New audio sync offset in milliseconds") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ inputName, inputAudioSyncOffset }) => { - try { - await client.sendRequest("SetInputAudioSyncOffset", { inputName, inputAudioSyncOffset }); - return { - content: [ - { - type: "text", - text: `Successfully set audio sync offset to ${inputAudioSyncOffset}ms for input: ${inputName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error setting input audio sync offset: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-set-input-audio-sync-offset", + title: "Set Input Audio Sync Offset", + description: "Sets the audio sync offset of an input", + requestType: "SetInputAudioSyncOffset", + inputSchema: z.object({ + inputName: z.string().describe("Name of the input to set the audio sync offset of"), + inputAudioSyncOffset: z.number().min(-950).max(20000).describe("New audio sync offset in milliseconds") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: ({ inputAudioSyncOffset, inputName }) => `Successfully set audio sync offset to ${inputAudioSyncOffset}ms for input: ${inputName}`, + }); // GetInputAudioMonitorType tool - server.registerTool( - "obs-get-input-audio-monitor-type", - { - title: "Get Input Audio Monitor Type", - description: "Gets the audio monitor type of an input", - inputSchema: z.object({ - inputName: z.string().describe("Name of the input to get the audio monitor type of") - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ inputName }) => { - try { - const response = await client.sendRequest("GetInputAudioMonitorType", { inputName }); - return { - content: [ - { - type: "text", - text: `Audio monitor type for input '${inputName}': ${response.monitorType}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting input audio monitor type: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-input-audio-monitor-type", + title: "Get Input Audio Monitor Type", + description: "Gets the audio monitor type of an input", + requestType: "GetInputAudioMonitorType", + inputSchema: z.object({ + inputName: z.string().describe("Name of the input to get the audio monitor type of") + }), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetInputAudioMonitorType tool - server.registerTool( - "obs-set-input-audio-monitor-type", - { - title: "Set Input Audio Monitor Type", - description: "Sets the audio monitor type of an input", - inputSchema: z.object({ - inputName: z.string().describe("Name of the input to set the audio monitor type of"), - monitorType: z.string().describe("Audio monitor type (OBS_MONITORING_TYPE_NONE, OBS_MONITORING_TYPE_MONITOR_ONLY, OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT)") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ inputName, monitorType }) => { - try { - await client.sendRequest("SetInputAudioMonitorType", { inputName, monitorType }); - return { - content: [ - { - type: "text", - text: `Successfully set audio monitor type to ${monitorType} for input: ${inputName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error setting input audio monitor type: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-set-input-audio-monitor-type", + title: "Set Input Audio Monitor Type", + description: "Sets the audio monitor type of an input", + requestType: "SetInputAudioMonitorType", + inputSchema: z.object({ + inputName: z.string().describe("Name of the input to set the audio monitor type of"), + monitorType: z.string().describe("Audio monitor type (OBS_MONITORING_TYPE_NONE, OBS_MONITORING_TYPE_MONITOR_ONLY, OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT)") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: ({ inputName, monitorType }) => `Successfully set audio monitor type to ${monitorType} for input: ${inputName}`, + }); } diff --git a/src/tools/media-inputs.ts b/src/tools/media-inputs.ts index fa2c69e..2cc9318 100644 --- a/src/tools/media-inputs.ts +++ b/src/tools/media-inputs.ts @@ -6,159 +6,67 @@ import { McpServer } from "@modelcontextprotocol/server"; import { OBSWebSocketClient } from "../client.js"; import { z } from "zod"; +import { registerObsRequestTool } from "./request-tool.js"; export function initialize(server: McpServer, client: OBSWebSocketClient): void { // GetMediaInputStatus tool - server.registerTool( - "obs-get-media-input-status", - { - title: "Get Media Input Status", - description: "Gets the status of a media input", - inputSchema: z.object({ - inputName: z.string().describe("Name of the media input") - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ inputName }) => { - try { - const response = await client.sendRequest("GetMediaInputStatus", { inputName }); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting media input status: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-media-input-status", + title: "Get Media Input Status", + description: "Gets the status of a media input", + requestType: "GetMediaInputStatus", + inputSchema: z.object({ + inputName: z.string().describe("Name of the media input") + }), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetMediaInputCursor tool - server.registerTool( - "obs-set-media-input-cursor", - { - title: "Set Media Input Cursor", - description: "Sets the cursor position of a media input", - inputSchema: z.object({ - inputName: z.string().describe("Name of the media input"), - mediaCursor: z.number().min(0).describe("New cursor position to set (in milliseconds)") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ inputName, mediaCursor }) => { - try { - await client.sendRequest("SetMediaInputCursor", { inputName, mediaCursor }); - return { - content: [ - { - type: "text", - text: `Successfully set media cursor position to ${mediaCursor}ms for input: ${inputName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error setting media input cursor: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-set-media-input-cursor", + title: "Set Media Input Cursor", + description: "Sets the cursor position of a media input", + requestType: "SetMediaInputCursor", + inputSchema: z.object({ + inputName: z.string().describe("Name of the media input"), + mediaCursor: z.number().min(0).describe("New cursor position to set (in milliseconds)") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: ({ inputName, mediaCursor }) => `Successfully set media cursor position to ${mediaCursor}ms for input: ${inputName}`, + }); // OffsetMediaInputCursor tool - server.registerTool( - "obs-offset-media-input-cursor", - { - title: "Offset Media Input Cursor", - description: "Offsets the current cursor position of a media input", - inputSchema: z.object({ - inputName: z.string().describe("Name of the media input"), - mediaCursorOffset: z.number().describe("Value to offset the current cursor position by (in milliseconds)") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, - }, - async ({ inputName, mediaCursorOffset }) => { - try { - await client.sendRequest("OffsetMediaInputCursor", { inputName, mediaCursorOffset }); - return { - content: [ - { - type: "text", - text: `Successfully offset media cursor position by ${mediaCursorOffset}ms for input: ${inputName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error offsetting media input cursor: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-offset-media-input-cursor", + title: "Offset Media Input Cursor", + description: "Offsets the current cursor position of a media input", + requestType: "OffsetMediaInputCursor", + inputSchema: z.object({ + inputName: z.string().describe("Name of the media input"), + mediaCursorOffset: z.number().describe("Value to offset the current cursor position by (in milliseconds)") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + successMessage: ({ inputName, mediaCursorOffset }) => `Successfully offset media cursor position by ${mediaCursorOffset}ms for input: ${inputName}`, + }); // TriggerMediaInputAction tool - server.registerTool( - "obs-trigger-media-input-action", - { - title: "Trigger Media Input Action", - description: "Triggers an action on a media input", - inputSchema: z.object({ - inputName: z.string().describe("Name of the media input"), - mediaAction: z.enum([ - "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PLAY", - "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PAUSE", - "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_STOP", - "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_RESTART", - "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_NEXT", - "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PREVIOUS" - ]).describe("Action to trigger (PLAY, PAUSE, STOP, RESTART, NEXT, PREVIOUS)") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, - }, - async ({ inputName, mediaAction }) => { - try { - await client.sendRequest("TriggerMediaInputAction", { inputName, mediaAction }); - return { - content: [ - { - type: "text", - text: `Successfully triggered media action '${mediaAction}' for input: ${inputName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error triggering media input action: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-trigger-media-input-action", + title: "Trigger Media Input Action", + description: "Triggers an action on a media input", + requestType: "TriggerMediaInputAction", + inputSchema: z.object({ + inputName: z.string().describe("Name of the media input"), + mediaAction: z.enum([ + "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PLAY", + "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PAUSE", + "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_STOP", + "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_RESTART", + "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_NEXT", + "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PREVIOUS" + ]).describe("Action to trigger (PLAY, PAUSE, STOP, RESTART, NEXT, PREVIOUS)") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + successMessage: ({ inputName, mediaAction }) => `Successfully triggered media action '${mediaAction}' for input: ${inputName}`, + }); } diff --git a/src/tools/outputs.ts b/src/tools/outputs.ts index b95bf8b..c5f159f 100644 --- a/src/tools/outputs.ts +++ b/src/tools/outputs.ts @@ -6,6 +6,7 @@ import { McpServer } from "@modelcontextprotocol/server"; import { OBSWebSocketClient } from "../client.js"; import { z } from "zod"; +import { registerObsRequestTool } from "./request-tool.js"; import { REPLAY_BUFFER_OUTPUT, startOutputAndConfirm, @@ -14,37 +15,13 @@ import { export function initialize(server: McpServer, client: OBSWebSocketClient): void { // GetVirtualCamStatus tool - server.registerTool( - "obs-get-virtual-cam-status", - { - title: "Get Virtual Camera Status", - description: "Gets the status of the virtualcam output", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const response = await client.sendRequest("GetVirtualCamStatus"); - return { - content: [ - { - type: "text", - text: `Virtual camera is ${response.outputActive ? "active" : "inactive"}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting virtual camera status: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-virtual-cam-status", + title: "Get Virtual Camera Status", + description: "Gets the status of the virtualcam output", + requestType: "GetVirtualCamStatus", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // ToggleVirtualCam tool server.registerTool( @@ -91,70 +68,23 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // StopVirtualCam tool - server.registerTool( - "obs-stop-virtual-cam", - { - title: "Stop Virtual Camera", - description: "Stops the virtualcam output", - annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - await client.sendRequest("StopVirtualCam"); - return { - content: [ - { - type: "text", - text: "Virtual camera stopped" - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error stopping virtual camera: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-stop-virtual-cam", + title: "Stop Virtual Camera", + description: "Stops the virtualcam output", + requestType: "StopVirtualCam", + annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }, + successMessage: () => "Virtual camera stopped", + }); // GetReplayBufferStatus tool - server.registerTool( - "obs-get-replay-buffer-status", - { - title: "Get Replay Buffer Status", - description: "Gets the status of the replay buffer output", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const response = await client.sendRequest("GetReplayBufferStatus"); - return { - content: [ - { - type: "text", - text: `Replay buffer is ${response.outputActive ? "active" : "inactive"}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting replay buffer status: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-replay-buffer-status", + title: "Get Replay Buffer Status", + description: "Gets the status of the replay buffer output", + requestType: "GetReplayBufferStatus", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // ToggleReplayBuffer tool server.registerTool( @@ -201,172 +131,54 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // StopReplayBuffer tool - server.registerTool( - "obs-stop-replay-buffer", - { - title: "Stop Replay Buffer", - description: "Stops the replay buffer output", - annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - await client.sendRequest("StopReplayBuffer"); - return { - content: [ - { - type: "text", - text: "Replay buffer stopped" - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error stopping replay buffer: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-stop-replay-buffer", + title: "Stop Replay Buffer", + description: "Stops the replay buffer output", + requestType: "StopReplayBuffer", + annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }, + successMessage: () => "Replay buffer stopped", + }); // SaveReplayBuffer tool - server.registerTool( - "obs-save-replay-buffer", - { - title: "Save Replay Buffer", - description: "Saves the contents of the replay buffer output", - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, - }, - async () => { - try { - await client.sendRequest("SaveReplayBuffer"); - return { - content: [ - { - type: "text", - text: "Replay buffer saved" - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error saving replay buffer: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-save-replay-buffer", + title: "Save Replay Buffer", + description: "Saves the contents of the replay buffer output", + requestType: "SaveReplayBuffer", + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + successMessage: () => "Replay buffer saved", + }); // GetLastReplayBufferReplay tool - server.registerTool( - "obs-get-last-replay-buffer-replay", - { - title: "Get Last Replay Buffer File", - description: "Gets the filename of the last replay buffer save file", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const response = await client.sendRequest("GetLastReplayBufferReplay"); - return { - content: [ - { - type: "text", - text: `Last replay buffer save file: ${response.savedReplayPath}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting last replay buffer file: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-last-replay-buffer-replay", + title: "Get Last Replay Buffer File", + description: "Gets the filename of the last replay buffer save file", + requestType: "GetLastReplayBufferReplay", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // GetOutputList tool - server.registerTool( - "obs-get-output-list", - { - title: "Get Output List", - description: "Gets the list of available outputs", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const response = await client.sendRequest("GetOutputList"); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting output list: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-output-list", + title: "Get Output List", + description: "Gets the list of available outputs", + requestType: "GetOutputList", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // GetOutputStatus tool - server.registerTool( - "obs-get-output-status", - { - title: "Get Output Status", - description: "Gets the status of an output", - inputSchema: z.object({ - outputName: z.string().describe("Output name") - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ outputName }) => { - try { - const response = await client.sendRequest("GetOutputStatus", { outputName }); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting output status: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-output-status", + title: "Get Output Status", + description: "Gets the status of an output", + requestType: "GetOutputStatus", + inputSchema: z.object({ + outputName: z.string().describe("Output name") + }), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // ToggleOutput tool server.registerTool( @@ -405,147 +217,54 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // StartOutput tool - server.registerTool( - "obs-start-output", - { - title: "Start Output", - description: "Starts an output", - inputSchema: z.object({ - outputName: z.string().describe("Output name") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, - }, - async ({ outputName }) => { - try { - await client.sendRequest("StartOutput", { outputName }); - return { - content: [ - { - type: "text", - text: `Output '${outputName}' started` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error starting output: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-start-output", + title: "Start Output", + description: "Starts an output", + requestType: "StartOutput", + inputSchema: z.object({ + outputName: z.string().describe("Output name") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + successMessage: ({ outputName }) => `Output '${outputName}' started`, + }); // StopOutput tool - server.registerTool( - "obs-stop-output", - { - title: "Stop Output", - description: "Stops an output", - inputSchema: z.object({ - outputName: z.string().describe("Output name") - }), - annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }, - }, - async ({ outputName }) => { - try { - await client.sendRequest("StopOutput", { outputName }); - return { - content: [ - { - type: "text", - text: `Output '${outputName}' stopped` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error stopping output: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-stop-output", + title: "Stop Output", + description: "Stops an output", + requestType: "StopOutput", + inputSchema: z.object({ + outputName: z.string().describe("Output name") + }), + annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }, + successMessage: ({ outputName }) => `Output '${outputName}' stopped`, + }); // GetOutputSettings tool - server.registerTool( - "obs-get-output-settings", - { - title: "Get Output Settings", - description: "Gets the settings of an output", - inputSchema: z.object({ - outputName: z.string().describe("Output name") - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ outputName }) => { - try { - const response = await client.sendRequest("GetOutputSettings", { outputName }); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting output settings: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-output-settings", + title: "Get Output Settings", + description: "Gets the settings of an output", + requestType: "GetOutputSettings", + inputSchema: z.object({ + outputName: z.string().describe("Output name") + }), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetOutputSettings tool - server.registerTool( - "obs-set-output-settings", - { - title: "Set Output Settings", - description: "Sets the settings of an output", - inputSchema: z.object({ - outputName: z.string().describe("Output name"), - outputSettings: z.record(z.string(), z.unknown()).describe("Output settings") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ outputName, outputSettings }) => { - try { - await client.sendRequest("SetOutputSettings", { outputName, outputSettings }); - return { - content: [ - { - type: "text", - text: `Settings updated for output '${outputName}'` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error setting output settings: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-set-output-settings", + title: "Set Output Settings", + description: "Sets the settings of an output", + requestType: "SetOutputSettings", + inputSchema: z.object({ + outputName: z.string().describe("Output name"), + outputSettings: z.record(z.string(), z.unknown()).describe("Output settings") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: ({ outputName }) => `Settings updated for output '${outputName}'`, + }); } diff --git a/src/tools/record.ts b/src/tools/record.ts index 06db806..abb948b 100644 --- a/src/tools/record.ts +++ b/src/tools/record.ts @@ -6,41 +6,18 @@ import { McpServer } from "@modelcontextprotocol/server"; import { OBSWebSocketClient } from "../client.js"; import { z } from "zod"; +import { registerObsRequestTool } from "./request-tool.js"; import { RECORD_OUTPUT, startOutputAndConfirm } from "./output-start.js"; export function initialize(server: McpServer, client: OBSWebSocketClient): void { // GetRecordStatus tool - server.registerTool( - "obs-get-record-status", - { - title: "Get Record Status", - description: "Gets the status of the record output", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const response = await client.sendRequest("GetRecordStatus"); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting record status: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-record-status", + title: "Get Record Status", + description: "Gets the status of the record output", + requestType: "GetRecordStatus", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // ToggleRecord tool server.registerTool( @@ -120,136 +97,44 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // ToggleRecordPause tool - server.registerTool( - "obs-toggle-record-pause", - { - title: "Toggle Record Pause", - description: "Toggles pause on the record output", - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, - }, - async () => { - try { - await client.sendRequest("ToggleRecordPause"); - return { - content: [ - { - type: "text", - text: "Recording pause toggled" - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error toggling record pause: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-toggle-record-pause", + title: "Toggle Record Pause", + description: "Toggles pause on the record output", + requestType: "ToggleRecordPause", + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + successMessage: () => "Recording pause toggled", + }); // PauseRecord tool - server.registerTool( - "obs-pause-record", - { - title: "Pause Recording", - description: "Pauses the record output", - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - await client.sendRequest("PauseRecord"); - return { - content: [ - { - type: "text", - text: "Recording paused" - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error pausing recording: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-pause-record", + title: "Pause Recording", + description: "Pauses the record output", + requestType: "PauseRecord", + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: () => "Recording paused", + }); // ResumeRecord tool - server.registerTool( - "obs-resume-record", - { - title: "Resume Recording", - description: "Resumes the record output", - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - await client.sendRequest("ResumeRecord"); - return { - content: [ - { - type: "text", - text: "Recording resumed" - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error resuming recording: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-resume-record", + title: "Resume Recording", + description: "Resumes the record output", + requestType: "ResumeRecord", + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: () => "Recording resumed", + }); // SplitRecordFile tool - server.registerTool( - "obs-split-record-file", - { - title: "Split Record File", - description: "Splits the current file being recorded into a new file", - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, - }, - async () => { - try { - await client.sendRequest("SplitRecordFile"); - return { - content: [ - { - type: "text", - text: "Recording file split" - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error splitting record file: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-split-record-file", + title: "Split Record File", + description: "Splits the current file being recorded into a new file", + requestType: "SplitRecordFile", + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + successMessage: () => "Recording file split", + }); // CreateRecordChapter tool server.registerTool( diff --git a/src/tools/request-tool.ts b/src/tools/request-tool.ts index 6407b42..d213866 100644 --- a/src/tools/request-tool.ts +++ b/src/tools/request-tool.ts @@ -43,11 +43,18 @@ type RequestToolDefinition = { inputSchema?: ZodObject; annotations: ToolAnnotations; responseMode?: "json" | "success"; + /** Replaces the JSON response with a confirmation built from the arguments. */ + successMessage?: (args: Record<string, any>) => string; /** Returns a refusal message when the request must not reach OBS. */ guard?: (client: OBSWebSocketClient, args: Record<string, unknown>) => Promise<string | undefined>; }; -function formatResponse(definition: RequestToolDefinition, response: unknown): string { +function formatResponse( + definition: RequestToolDefinition, + response: unknown, + requestData: Record<string, unknown>, +): string { + if (definition.successMessage) return definition.successMessage(requestData); if (definition.responseMode === "success") { return `${definition.title} completed successfully`; } @@ -70,7 +77,7 @@ async function executeRequest( } const response = await client.sendRequest(definition.requestType, requestData); return { - content: [{ type: "text" as const, text: formatResponse(definition, response) }], + content: [{ type: "text" as const, text: formatResponse(definition, response, requestData ?? {}) }], }; } catch (error) { return { diff --git a/src/tools/scene-items.ts b/src/tools/scene-items.ts index 0bb7846..10f6e3a 100644 --- a/src/tools/scene-items.ts +++ b/src/tools/scene-items.ts @@ -6,45 +6,22 @@ import { McpServer } from "@modelcontextprotocol/server"; import { OBSWebSocketClient } from "../client.js"; import { z } from "zod"; +import { registerObsRequestTool } from "./request-tool.js"; export function initialize(server: McpServer, client: OBSWebSocketClient): void { // GetSceneItemList tool - server.registerTool( - "obs-get-scene-items", - { - title: "Get Scene Items", - description: "Get a list of all scene items in a scene", - inputSchema: z.object({ - canvasUuid: z.string().optional().describe("UUID of the canvas containing the scene"), - sceneName: z.string().optional().describe("Name of the scene to get items from"), - sceneUuid: z.string().optional().describe("UUID of the scene to get items from") - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ canvasUuid, sceneName, sceneUuid }) => { - try { - const sceneItems = await client.sendRequest("GetSceneItemList", { canvasUuid, sceneName, sceneUuid }); - return { - content: [ - { - type: "text", - text: JSON.stringify(sceneItems, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting scene items: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-scene-items", + title: "Get Scene Items", + description: "Get a list of all scene items in a scene", + requestType: "GetSceneItemList", + inputSchema: z.object({ + canvasUuid: z.string().optional().describe("UUID of the canvas containing the scene"), + sceneName: z.string().optional().describe("Name of the scene to get items from"), + sceneUuid: z.string().optional().describe("UUID of the scene to get items from") + }), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // CreateSceneItem tool server.registerTool( @@ -96,44 +73,20 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // RemoveSceneItem tool - server.registerTool( - "obs-remove-scene-item", - { - title: "Remove Scene Item", - description: "Remove a scene item from a scene", - inputSchema: z.object({ - canvasUuid: z.string().optional().describe("UUID of the canvas containing the scene"), - sceneName: z.string().optional().describe("Name of the scene to remove the item from"), - sceneUuid: z.string().optional().describe("UUID of the scene to remove the item from"), - sceneItemId: z.number().int().nonnegative().describe("The ID of the scene item to remove") - }), - annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }, - }, - async ({ canvasUuid, sceneName, sceneUuid, sceneItemId }) => { - try { - await client.sendRequest("RemoveSceneItem", { canvasUuid, sceneName, sceneUuid, sceneItemId }); - - return { - content: [ - { - type: "text", - text: `Successfully removed item with ID ${sceneItemId} from ${sceneName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error removing scene item: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-remove-scene-item", + title: "Remove Scene Item", + description: "Remove a scene item from a scene", + requestType: "RemoveSceneItem", + inputSchema: z.object({ + canvasUuid: z.string().optional().describe("UUID of the canvas containing the scene"), + sceneName: z.string().optional().describe("Name of the scene to remove the item from"), + sceneUuid: z.string().optional().describe("UUID of the scene to remove the item from"), + sceneItemId: z.number().int().nonnegative().describe("The ID of the scene item to remove") + }), + annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }, + successMessage: ({ sceneItemId, sceneName }) => `Successfully removed item with ID ${sceneItemId} from ${sceneName}`, + }); // SetSceneItemEnabled tool server.registerTool( @@ -183,43 +136,19 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // GetSceneItemTransform tool - server.registerTool( - "obs-get-scene-item-transform", - { - title: "Get Scene Item Transform", - description: "Get the position, rotation, scale, or crop of a scene item", - inputSchema: z.object({ - canvasUuid: z.string().optional().describe("UUID of the canvas containing the scene"), - sceneName: z.string().optional().describe("Name of the scene containing the item"), - sceneUuid: z.string().optional().describe("UUID of the scene containing the item"), - sceneItemId: z.number().int().nonnegative().describe("The ID of the scene item") - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ canvasUuid, sceneName, sceneUuid, sceneItemId }) => { - try { - const response = await client.sendRequest("GetSceneItemTransform", { canvasUuid, sceneName, sceneUuid, sceneItemId }); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting scene item transform: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-scene-item-transform", + title: "Get Scene Item Transform", + description: "Get the position, rotation, scale, or crop of a scene item", + requestType: "GetSceneItemTransform", + inputSchema: z.object({ + canvasUuid: z.string().optional().describe("UUID of the canvas containing the scene"), + sceneName: z.string().optional().describe("Name of the scene containing the item"), + sceneUuid: z.string().optional().describe("UUID of the scene containing the item"), + sceneItemId: z.number().int().nonnegative().describe("The ID of the scene item") + }), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetSceneItemTransform tool server.registerTool( @@ -306,51 +235,19 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // GetSceneItemIdByName tool - server.registerTool( - "obs-get-scene-item-id", - { - title: "Get Scene Item ID", - description: "Get the ID of a scene item by its source name", - inputSchema: z.object({ - canvasUuid: z.string().optional().describe("UUID of the canvas containing the scene"), - sceneName: z.string().optional().describe("Name of the scene to search"), - sceneUuid: z.string().optional().describe("UUID of the scene to search"), - sourceName: z.string().optional().describe("Name of the source to find"), - sourceUuid: z.string().optional().describe("UUID of the source to find"), - searchOffset: z.number().int().nonnegative().optional().describe("Match offset when the source appears multiple times") - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ canvasUuid, sceneName, sceneUuid, sourceName, sourceUuid, searchOffset }) => { - try { - const response = await client.sendRequest("GetSceneItemId", { - canvasUuid, - sceneName, - sceneUuid, - sourceName, - sourceUuid, - searchOffset - }); - - return { - content: [ - { - type: "text", - text: `Scene item ID for ${sourceName} in ${sceneName}: ${response.sceneItemId}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting scene item ID: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-scene-item-id", + title: "Get Scene Item ID", + description: "Get the ID of a scene item by its source name", + requestType: "GetSceneItemId", + inputSchema: z.object({ + canvasUuid: z.string().optional().describe("UUID of the canvas containing the scene"), + sceneName: z.string().optional().describe("Name of the scene to search"), + sceneUuid: z.string().optional().describe("UUID of the scene to search"), + sourceName: z.string().optional().describe("Name of the source to find"), + sourceUuid: z.string().optional().describe("UUID of the source to find"), + searchOffset: z.number().int().nonnegative().optional().describe("Match offset when the source appears multiple times") + }), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); } diff --git a/src/tools/scenes.ts b/src/tools/scenes.ts index 48abd49..16ce1e7 100644 --- a/src/tools/scenes.ts +++ b/src/tools/scenes.ts @@ -6,286 +6,100 @@ import { McpServer } from "@modelcontextprotocol/server"; import { OBSWebSocketClient } from "../client.js"; import { z } from "zod"; +import { registerObsRequestTool } from "./request-tool.js"; export function initialize(server: McpServer, client: OBSWebSocketClient): void { // GetSceneList tool - server.registerTool( - "obs-get-scene-list", - { - title: "Get Scene List", - description: "Get a list of scenes in OBS", - inputSchema: z.object({ - canvasUuid: z.string().optional().describe("UUID of the canvas to list scenes from") - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ canvasUuid }) => { - try { - const sceneList = await client.sendRequest("GetSceneList", { canvasUuid }); - return { - content: [ - { - type: "text", - text: JSON.stringify(sceneList, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting scene list: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-scene-list", + title: "Get Scene List", + description: "Get a list of scenes in OBS", + requestType: "GetSceneList", + inputSchema: z.object({ + canvasUuid: z.string().optional().describe("UUID of the canvas to list scenes from") + }), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // GetCurrentProgramScene tool - server.registerTool( - "obs-get-current-scene", - { - title: "Get Current Scene", - description: "Get the current active scene in OBS", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const currentScene = await client.sendRequest("GetCurrentProgramScene"); - return { - content: [ - { - type: "text", - text: `Current scene: ${currentScene.currentProgramSceneName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting current scene: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-current-scene", + title: "Get Current Scene", + description: "Get the current active scene in OBS", + requestType: "GetCurrentProgramScene", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetCurrentProgramScene tool - server.registerTool( - "obs-set-current-scene", - { - title: "Set Current Scene", - description: "Set the current active scene in OBS", - inputSchema: z.object({ - sceneName: z.string().describe("The name of the scene to set as current") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ sceneName }) => { - try { - await client.sendRequest("SetCurrentProgramScene", { sceneName }); - return { - content: [ - { - type: "text", - text: `Successfully switched to scene: ${sceneName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error setting current scene: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-set-current-scene", + title: "Set Current Scene", + description: "Set the current active scene in OBS", + requestType: "SetCurrentProgramScene", + inputSchema: z.object({ + sceneName: z.string().describe("The name of the scene to set as current") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: ({ sceneName }) => `Successfully switched to scene: ${sceneName}`, + }); // GetCurrentPreviewScene tool (Studio Mode) - server.registerTool( - "obs-get-preview-scene", - { - title: "Get Preview Scene", - description: "Get the current preview scene in OBS Studio Mode", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const previewScene = await client.sendRequest("GetCurrentPreviewScene"); - return { - content: [ - { - type: "text", - text: `Preview scene: ${previewScene.currentPreviewSceneName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting preview scene: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-preview-scene", + title: "Get Preview Scene", + description: "Get the current preview scene in OBS Studio Mode", + requestType: "GetCurrentPreviewScene", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetCurrentPreviewScene tool (Studio Mode) - server.registerTool( - "obs-set-preview-scene", - { - title: "Set Preview Scene", - description: "Set the current preview scene in OBS Studio Mode", - inputSchema: z.object({ - sceneName: z.string().describe("The name of the scene to set as preview") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ sceneName }) => { - try { - await client.sendRequest("SetCurrentPreviewScene", { sceneName }); - return { - content: [ - { - type: "text", - text: `Successfully set preview scene to: ${sceneName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error setting preview scene: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-set-preview-scene", + title: "Set Preview Scene", + description: "Set the current preview scene in OBS Studio Mode", + requestType: "SetCurrentPreviewScene", + inputSchema: z.object({ + sceneName: z.string().describe("The name of the scene to set as preview") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: ({ sceneName }) => `Successfully set preview scene to: ${sceneName}`, + }); // CreateScene tool - server.registerTool( - "obs-create-scene", - { - title: "Create Scene", - description: "Create a new scene in OBS", - inputSchema: z.object({ - canvasUuid: z.string().optional().describe("UUID of the canvas to create the scene in"), - sceneName: z.string().describe("The name for the new scene") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, - }, - async ({ canvasUuid, sceneName }) => { - try { - await client.sendRequest("CreateScene", { canvasUuid, sceneName }); - return { - content: [ - { - type: "text", - text: `Successfully created scene: ${sceneName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error creating scene: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-create-scene", + title: "Create Scene", + description: "Create a new scene in OBS", + requestType: "CreateScene", + inputSchema: z.object({ + canvasUuid: z.string().optional().describe("UUID of the canvas to create the scene in"), + sceneName: z.string().describe("The name for the new scene") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + successMessage: ({ sceneName }) => `Successfully created scene: ${sceneName}`, + }); // RemoveScene tool - server.registerTool( - "obs-remove-scene", - { - title: "Remove Scene", - description: "Remove a scene from OBS", - inputSchema: z.object({ - canvasUuid: z.string().optional().describe("UUID of the canvas containing the scene"), - sceneName: z.string().describe("The name of the scene to remove") - }), - annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }, - }, - async ({ canvasUuid, sceneName }) => { - try { - await client.sendRequest("RemoveScene", { canvasUuid, sceneName }); - return { - content: [ - { - type: "text", - text: `Successfully removed scene: ${sceneName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error removing scene: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-remove-scene", + title: "Remove Scene", + description: "Remove a scene from OBS", + requestType: "RemoveScene", + inputSchema: z.object({ + canvasUuid: z.string().optional().describe("UUID of the canvas containing the scene"), + sceneName: z.string().describe("The name of the scene to remove") + }), + annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }, + successMessage: ({ sceneName }) => `Successfully removed scene: ${sceneName}`, + }); // TriggerStudioModeTransition tool - server.registerTool( - "obs-trigger-studio-transition", - { - title: "Trigger Studio Transition", - description: "Trigger a transition from preview to program scene in Studio Mode", - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, - }, - async () => { - try { - await client.sendRequest("TriggerStudioModeTransition"); - return { - content: [ - { - type: "text", - text: "Successfully triggered studio mode transition" - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error triggering studio transition: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-trigger-studio-transition", + title: "Trigger Studio Transition", + description: "Trigger a transition from preview to program scene in Studio Mode", + requestType: "TriggerStudioModeTransition", + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + successMessage: () => "Successfully triggered studio mode transition", + }); } diff --git a/src/tools/sources.ts b/src/tools/sources.ts index 5d701f1..a11f6e6 100644 --- a/src/tools/sources.ts +++ b/src/tools/sources.ts @@ -7,50 +7,22 @@ import { McpServer } from "@modelcontextprotocol/server"; import { OBSWebSocketClient } from "../client.js"; import { screenshotResult } from "./screenshot.js"; import { z } from "zod"; +import { registerObsRequestTool } from "./request-tool.js"; export function initialize(server: McpServer, client: OBSWebSocketClient): void { // GetSourceActive tool - server.registerTool( - "obs-get-source-active", - { - title: "Get Source Active State", - description: "Gets the active and show state of a source", - inputSchema: z.object({ - canvasUuid: z.string().optional().describe("UUID of the canvas containing the source"), - sourceName: z.string().optional().describe("Name of the source to get the active state of"), - sourceUuid: z.string().optional().describe("UUID of the source to get the active state of") - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ canvasUuid, sourceName, sourceUuid }) => { - try { - const response = await client.sendRequest("GetSourceActive", { - canvasUuid, - sourceName, - sourceUuid - }); - - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting source active state: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-source-active", + title: "Get Source Active State", + description: "Gets the active and show state of a source", + requestType: "GetSourceActive", + inputSchema: z.object({ + canvasUuid: z.string().optional().describe("UUID of the canvas containing the source"), + sourceName: z.string().optional().describe("Name of the source to get the active state of"), + sourceUuid: z.string().optional().describe("UUID of the source to get the active state of") + }), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // GetSourceScreenshot tool server.registerTool( @@ -101,55 +73,22 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // SaveSourceScreenshot tool - server.registerTool( - "obs-save-source-screenshot", - { - title: "Save Source Screenshot", - description: "Saves a screenshot of a source to the filesystem", - inputSchema: z.object({ - canvasUuid: z.string().optional().describe("UUID of the canvas containing the source"), - sourceName: z.string().optional().describe("Name of the source to take a screenshot of"), - sourceUuid: z.string().optional().describe("UUID of the source to take a screenshot of"), - imageFormat: z.string().describe("Image compression format to use"), - imageFilePath: z.string().describe("Path to save the screenshot file to"), - imageWidth: z.number().optional().describe("Width to scale the screenshot to"), - imageHeight: z.number().optional().describe("Height to scale the screenshot to"), - imageCompressionQuality: z.number().optional().describe("Compression quality to use (0-100, -1 for default)") - }), - annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }, - }, - async ({ canvasUuid, sourceName, sourceUuid, imageFormat, imageFilePath, imageWidth, imageHeight, imageCompressionQuality }) => { - try { - await client.sendRequest("SaveSourceScreenshot", { - canvasUuid, - sourceName, - sourceUuid, - imageFormat, - imageFilePath, - imageWidth, - imageHeight, - imageCompressionQuality - }); - - return { - content: [ - { - type: "text", - text: `Successfully saved screenshot to: ${imageFilePath}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error saving source screenshot: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-save-source-screenshot", + title: "Save Source Screenshot", + description: "Saves a screenshot of a source to the filesystem", + requestType: "SaveSourceScreenshot", + inputSchema: z.object({ + canvasUuid: z.string().optional().describe("UUID of the canvas containing the source"), + sourceName: z.string().optional().describe("Name of the source to take a screenshot of"), + sourceUuid: z.string().optional().describe("UUID of the source to take a screenshot of"), + imageFormat: z.string().describe("Image compression format to use"), + imageFilePath: z.string().describe("Path to save the screenshot file to"), + imageWidth: z.number().optional().describe("Width to scale the screenshot to"), + imageHeight: z.number().optional().describe("Height to scale the screenshot to"), + imageCompressionQuality: z.number().optional().describe("Compression quality to use (0-100, -1 for default)") + }), + annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }, + successMessage: ({ imageFilePath }) => `Successfully saved screenshot to: ${imageFilePath}`, + }); } diff --git a/src/tools/streaming.ts b/src/tools/streaming.ts index 7ddbfb4..6483766 100644 --- a/src/tools/streaming.ts +++ b/src/tools/streaming.ts @@ -6,41 +6,18 @@ import { McpServer } from "@modelcontextprotocol/server"; import { OBSWebSocketClient } from "../client.js"; import { z } from "zod"; +import { registerObsRequestTool } from "./request-tool.js"; import { startOutputAndConfirm, STREAM_OUTPUT } from "./output-start.js"; export function initialize(server: McpServer, client: OBSWebSocketClient): void { // GetStreamStatus tool - server.registerTool( - "obs-get-stream-status", - { - title: "Get Stream Status", - description: "Get the current streaming status", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const status = await client.sendRequest("GetStreamStatus"); - return { - content: [ - { - type: "text", - text: JSON.stringify(status, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting stream status: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-stream-status", + title: "Get Stream Status", + description: "Get the current streaming status", + requestType: "GetStreamStatus", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // StartStream tool server.registerTool( @@ -54,37 +31,14 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // StopStream tool - server.registerTool( - "obs-stop-stream", - { - title: "Stop Stream", - description: "Stop streaming in OBS", - annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }, - }, - async () => { - try { - await client.sendRequest("StopStream"); - return { - content: [ - { - type: "text", - text: "Successfully stopped streaming" - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error stopping stream: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-stop-stream", + title: "Stop Stream", + description: "Stop streaming in OBS", + requestType: "StopStream", + annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }, + successMessage: () => "Successfully stopped streaming", + }); // ToggleStream tool server.registerTool( @@ -120,38 +74,15 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // SendStreamCaption tool - server.registerTool( - "obs-send-stream-caption", - { - title: "Send Stream Caption", - description: "Sends CEA-608 caption text over the stream output", - inputSchema: z.object({ - captionText: z.string().describe("Caption text to send") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, - }, - async ({ captionText }) => { - try { - await client.sendRequest("SendStreamCaption", { captionText }); - return { - content: [ - { - type: "text", - text: "Successfully sent stream caption" - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error sending stream caption: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-send-stream-caption", + title: "Send Stream Caption", + description: "Sends CEA-608 caption text over the stream output", + requestType: "SendStreamCaption", + inputSchema: z.object({ + captionText: z.string().describe("Caption text to send") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + successMessage: () => "Successfully sent stream caption", + }); } diff --git a/src/tools/transitions.ts b/src/tools/transitions.ts index c9dc7b7..c17a12e 100644 --- a/src/tools/transitions.ts +++ b/src/tools/transitions.ts @@ -6,142 +6,48 @@ import { McpServer } from "@modelcontextprotocol/server"; import { OBSWebSocketClient } from "../client.js"; import { z } from "zod"; +import { registerObsRequestTool } from "./request-tool.js"; export function initialize(server: McpServer, client: OBSWebSocketClient): void { // GetSceneTransitionList tool - server.registerTool( - "obs-get-transition-list", - { - title: "Get Transition List", - description: "Get a list of available transitions in OBS", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const transitions = await client.sendRequest("GetSceneTransitionList"); - return { - content: [ - { - type: "text", - text: JSON.stringify(transitions, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting transition list: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-transition-list", + title: "Get Transition List", + description: "Get a list of available transitions in OBS", + requestType: "GetSceneTransitionList", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // GetCurrentTransition tool - server.registerTool( - "obs-get-current-transition", - { - title: "Get Current Transition", - description: "Get the name of the currently active transition", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const transition = await client.sendRequest("GetCurrentSceneTransition"); - return { - content: [ - { - type: "text", - text: `Current transition: ${transition.transitionName} (${transition.transitionKind}) with duration: ${transition.transitionDuration}ms` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting current transition: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-current-transition", + title: "Get Current Transition", + description: "Get the name of the currently active transition", + requestType: "GetCurrentSceneTransition", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetCurrentTransition tool - server.registerTool( - "obs-set-current-transition", - { - title: "Set Current Transition", - description: "Set the current transition in OBS", - inputSchema: z.object({ - transitionName: z.string().describe("The name of the transition to set as current") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ transitionName }) => { - try { - await client.sendRequest("SetCurrentSceneTransition", { transitionName }); - return { - content: [ - { - type: "text", - text: `Successfully set current transition to: ${transitionName}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error setting current transition: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-set-current-transition", + title: "Set Current Transition", + description: "Set the current transition in OBS", + requestType: "SetCurrentSceneTransition", + inputSchema: z.object({ + transitionName: z.string().describe("The name of the transition to set as current") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: ({ transitionName }) => `Successfully set current transition to: ${transitionName}`, + }); // GetTransitionDuration tool - server.registerTool( - "obs-get-transition-duration", - { - title: "Get Transition Duration", - description: "Get the duration of the current transition in milliseconds", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const duration = await client.sendRequest("GetCurrentSceneTransition"); - return { - content: [ - { - type: "text", - text: `Current transition duration: ${duration.transitionDuration}ms` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting transition duration: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-transition-duration", + title: "Get Transition Duration", + description: "Get the duration of the current transition in milliseconds", + requestType: "GetCurrentSceneTransition", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetTransitionDuration tool server.registerTool( @@ -180,137 +86,43 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // GetTransitionKind tool - server.registerTool( - "obs-get-transition-kind", - { - title: "Get Transition Kind", - description: "Get the kind/type of the current transition", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const transition = await client.sendRequest("GetCurrentSceneTransition"); - return { - content: [ - { - type: "text", - text: `Current transition kind: ${transition.transitionKind}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting transition kind: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-transition-kind", + title: "Get Transition Kind", + description: "Get the kind/type of the current transition", + requestType: "GetCurrentSceneTransition", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetTransitionSettings tool - server.registerTool( - "obs-set-transition-settings", - { - title: "Set Transition Settings", - description: "Set the settings of the current transition", - inputSchema: z.object({ - transitionSettings: z.record(z.string(), z.unknown()).describe("The settings to apply to the transition") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ transitionSettings }) => { - try { - await client.sendRequest("SetCurrentSceneTransitionSettings", { transitionSettings }); - return { - content: [ - { - type: "text", - text: "Successfully updated current transition settings" - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error setting transition settings: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-set-transition-settings", + title: "Set Transition Settings", + description: "Set the settings of the current transition", + requestType: "SetCurrentSceneTransitionSettings", + inputSchema: z.object({ + transitionSettings: z.record(z.string(), z.unknown()).describe("The settings to apply to the transition") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: () => "Successfully updated current transition settings", + }); // GetTransitionSettings tool - server.registerTool( - "obs-get-transition-settings", - { - title: "Get Transition Settings", - description: "Get the settings of the current transition", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const settings = await client.sendRequest("GetCurrentSceneTransition"); - return { - content: [ - { - type: "text", - text: JSON.stringify(settings, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting transition settings: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-transition-settings", + title: "Get Transition Settings", + description: "Get the settings of the current transition", + requestType: "GetCurrentSceneTransition", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // TriggerStudioModeTransition tool - server.registerTool( - "obs-trigger-transition", - { - title: "Trigger Transition", - description: "Trigger a scene transition in OBS (Studio Mode must be enabled)", - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, - }, - async () => { - try { - await client.sendRequest("TriggerStudioModeTransition"); - return { - content: [ - { - type: "text", - text: "Successfully triggered studio mode transition" - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error triggering transition: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-trigger-transition", + title: "Trigger Transition", + description: "Trigger a scene transition in OBS (Studio Mode must be enabled)", + requestType: "TriggerStudioModeTransition", + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + successMessage: () => "Successfully triggered studio mode transition", + }); } diff --git a/src/tools/ui.ts b/src/tools/ui.ts index 51933d6..079c8cf 100644 --- a/src/tools/ui.ts +++ b/src/tools/ui.ts @@ -6,76 +6,30 @@ import { McpServer } from "@modelcontextprotocol/server"; import { OBSWebSocketClient } from "../client.js"; import { z } from "zod"; +import { registerObsRequestTool } from "./request-tool.js"; export function initialize(server: McpServer, client: OBSWebSocketClient): void { // GetStudioModeEnabled tool - server.registerTool( - "obs-get-studio-mode", - { - title: "Get Studio Mode", - description: "Gets whether studio mode is enabled", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const response = await client.sendRequest("GetStudioModeEnabled"); - return { - content: [ - { - type: "text", - text: `Studio Mode is ${response.studioModeEnabled ? "enabled" : "disabled"}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error checking studio mode: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-studio-mode", + title: "Get Studio Mode", + description: "Gets whether studio mode is enabled", + requestType: "GetStudioModeEnabled", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // SetStudioModeEnabled tool - server.registerTool( - "obs-set-studio-mode", - { - title: "Set Studio Mode", - description: "Enables or disables studio mode", - inputSchema: z.object({ - studioModeEnabled: z.boolean().describe("Whether to enable (true) or disable (false) Studio Mode") - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async ({ studioModeEnabled }) => { - try { - await client.sendRequest("SetStudioModeEnabled", { studioModeEnabled }); - return { - content: [ - { - type: "text", - text: `Studio Mode has been ${studioModeEnabled ? "enabled" : "disabled"}` - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error setting studio mode: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-set-studio-mode", + title: "Set Studio Mode", + description: "Enables or disables studio mode", + requestType: "SetStudioModeEnabled", + inputSchema: z.object({ + studioModeEnabled: z.boolean().describe("Whether to enable (true) or disable (false) Studio Mode") + }), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + successMessage: ({ studioModeEnabled }) => `Studio Mode has been ${studioModeEnabled ? "enabled" : "disabled"}`, + }); // OpenInputPropertiesDialog tool server.registerTool( @@ -216,37 +170,13 @@ export function initialize(server: McpServer, client: OBSWebSocketClient): void ); // GetMonitorList tool - server.registerTool( - "obs-get-monitor-list", - { - title: "Get Monitor List", - description: "Gets a list of connected monitors and information about them", - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, - }, - async () => { - try { - const response = await client.sendRequest("GetMonitorList"); - return { - content: [ - { - type: "text", - text: JSON.stringify(response, null, 2) - } - ] - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error getting monitor list: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }; - } - } - ); + registerObsRequestTool(server, client, { + name: "obs-get-monitor-list", + title: "Get Monitor List", + description: "Gets a list of connected monitors and information about them", + requestType: "GetMonitorList", + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + }); // OpenVideoMixProjector tool server.registerTool( From e36992d0ce811914b48622922bc3586d4c17efc6 Mon Sep 17 00:00:00 2001 From: Tom Main <tom@execufunction.com> Date: Tue, 22 Sep 2026 17:50:02 -0400 Subject: [PATCH 2/3] Add tool groups and read-only mode OBS_MCP_TOOLSETS (all, core, or group names), OBS_MCP_TOOLS (extra tool names), and OBS_MCP_READ_ONLY choose which tools are registered; read-only mode overrides both lists. Unset, every tool is registered as before. Unknown group or tool names stop the server at startup. The .mcpb manifest exposes the group list and read-only switch as user settings. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vjn8uBEa4mJshaQU3pFuqX --- README.md | 26 ++++++ manifest.json | 37 +++++++- src/server.ts | 23 ++++- src/tools/index.ts | 52 ++++++++---- src/tools/toolsets.test.ts | 92 ++++++++++++++++++++ src/tools/toolsets.ts | 167 +++++++++++++++++++++++++++++++++++++ test/e2e/stdio.e2e.test.ts | 38 +++++++++ 7 files changed, 412 insertions(+), 23 deletions(-) create mode 100644 src/tools/toolsets.test.ts create mode 100644 src/tools/toolsets.ts diff --git a/README.md b/README.md index 37ca415..3efd7cb 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,9 @@ Open that file in an MCPB-compatible desktop client. The package prompts for the | `OBS_WEBSOCKET_URL` | No | `ws://localhost:4455` | Address of the OBS WebSocket server | | `OBS_WEBSOCKET_PASSWORD` | Only when OBS authentication is enabled | None | Password configured in OBS | | `OBS_MCP_MAX_SCREENSHOT_BYTES` | No | `4194304` | Maximum decoded size of a screenshot returned through MCP; the hard ceiling is 6 MiB | +| `OBS_MCP_TOOLSETS` | No | all tools | Comma-separated tool groups to register: `all`, `core`, or any group listed under [Choosing tools](#choosing-tools) | +| `OBS_MCP_TOOLS` | No | none | Comma-separated tool names to register in addition to `OBS_MCP_TOOLSETS` | +| `OBS_MCP_READ_ONLY` | No | `false` | Register only read-only tools; overrides both lists | | `OBS_MCP_LOG_LEVEL` | No | `info` | Diagnostic output on stderr: `debug`, `info`, `error`, or `silent` | The server never prints the password. Connection and protocol diagnostics are written to stderr so stdout remains reserved for MCP messages. @@ -114,6 +117,29 @@ The 156 tools are organized around the OBS protocol rather than a smaller opinio Tool discovery works without an OBS connection. Calls that need OBS return an MCP error until the WebSocket connection is ready. +### Choosing tools + +Every tool is registered by default. Clients load each registered tool's definition into the model's context, so a smaller set is cheaper and easier for the model to choose from. + +| Group | Covers | +|---|---| +| `general` | status, version, statistics, hotkeys, vendor requests, custom events, sleep | +| `scenes` | scenes, canvases, program and preview scene | +| `scene-items` | scene items, groups, transforms, ordering, locking, blend modes | +| `sources` | source screenshots and active state | +| `inputs` | inputs, input settings and properties, audio, deinterlacing | +| `media` | media input playback | +| `filters` | source filters | +| `transitions` | transitions, overrides, the T-Bar | +| `record` | recording, chapters, and `obs-preflight` | +| `stream` | streaming and captions | +| `outputs` | virtual camera, replay buffer, and generic outputs | +| `config` | profiles, scene collections, video settings, persistent data | +| `ui` | studio mode, dialogs, projectors | +| `protocol` | `obs-describe-request` and the generic `obs-call-request` | + +`core` expands to `general`, `scenes`, `scene-items`, `sources`, `inputs`, and `record` (79 tools). `OBS_MCP_TOOLSETS=core OBS_MCP_READ_ONLY=true` gives a 38-tool inspection-only server. The server refuses to start when a group or tool name is unknown. + ## Where it fits OBS MCP is well suited to operational work that already has a clear outcome: inspecting a scene collection, creating or arranging sources, changing audio state, switching scenes, taking screenshots, checking output status, or carrying out a repeatable recording setup. diff --git a/manifest.json b/manifest.json index 9844377..d140de0 100644 --- a/manifest.json +++ b/manifest.json @@ -19,9 +19,20 @@ "support": "https://github.com/Tom-R-Main/OBS-MCP/issues", "license": "GPL-2.0-only", "tools_generated": true, - "keywords": ["obs", "streaming", "recording", "video", "broadcast", "scenes"], + "keywords": [ + "obs", + "streaming", + "recording", + "video", + "broadcast", + "scenes" + ], "compatibility": { - "platforms": ["darwin", "win32", "linux"], + "platforms": [ + "darwin", + "win32", + "linux" + ], "runtimes": { "node": ">=20.19.0" } @@ -31,10 +42,14 @@ "entry_point": "server/index.js", "mcp_config": { "command": "node", - "args": ["${__dirname}/server/index.js"], + "args": [ + "${__dirname}/server/index.js" + ], "env": { "OBS_WEBSOCKET_URL": "${user_config.obs_websocket_url}", - "OBS_WEBSOCKET_PASSWORD": "${user_config.obs_websocket_password}" + "OBS_WEBSOCKET_PASSWORD": "${user_config.obs_websocket_password}", + "OBS_MCP_TOOLSETS": "${user_config.obs_mcp_toolsets}", + "OBS_MCP_READ_ONLY": "${user_config.obs_mcp_read_only}" } } }, @@ -52,6 +67,20 @@ "description": "Password from OBS WebSocket Server Settings. Leave empty if authentication is disabled.", "required": false, "sensitive": true + }, + "obs_mcp_toolsets": { + "type": "string", + "title": "Tool groups", + "description": "Comma-separated tool groups to expose: all, core, or any of general, scenes, scene-items, sources, inputs, media, filters, transitions, record, stream, outputs, config, ui, protocol.", + "required": false, + "default": "all" + }, + "obs_mcp_read_only": { + "type": "boolean", + "title": "Read-only mode", + "description": "Expose only tools that do not change OBS.", + "required": false, + "default": false } } } diff --git a/src/server.ts b/src/server.ts index 6077da6..857d179 100644 --- a/src/server.ts +++ b/src/server.ts @@ -8,6 +8,7 @@ import { McpServer } from "@modelcontextprotocol/server"; import { OBSWebSocketClient } from "./client.js"; import * as tools from "./tools/index.js"; import { PACKAGE_VERSION } from "./version.js"; +import { ALL_TOOLS, assertKnownTools, parseToolFilter, type ToolFilter } from "./tools/toolsets.js"; import { logger } from "./logger.js"; // Create the OBS WebSocket client @@ -91,19 +92,39 @@ obsClient.on("disconnected", () => { } }); +// Parsed once at startup so a bad OBS_MCP_TOOLSETS/OBS_MCP_TOOLS value fails fast. +let toolFilter: ToolFilter | null = null; + export function createServer(): McpServer { const server = new McpServer({ name: "obs-mcp", version: PACKAGE_VERSION, }); - tools.initialize(server, obsClient); + tools.initialize(server, obsClient, toolFilter ?? ALL_TOOLS); return server; } +function loadToolFilter(): ToolFilter { + const filter = parseToolFilter(); + const registry = tools.initialize( + new McpServer({ name: "obs-mcp-validation", version: PACKAGE_VERSION }), + obsClient, + filter, + ); + assertKnownTools(filter, registry); + const count = registry.filter(({ registered }) => registered).length; + if (count === 0) throw new Error("The tool filter excludes every tool"); + if (filter.groups !== null || filter.readOnly) { + logger.log(`Registering ${count} of ${registry.length} tools${filter.readOnly ? " (read-only)" : ""}`); + } + return filter; +} + // Set up server startup logic export async function startServer(): Promise<void> { try { + toolFilter = loadToolFilter(); stdioServer = serveStdio(createServer, { onerror: (error) => logger.error(`MCP stdio error: ${error.message}`), }); diff --git a/src/tools/index.ts b/src/tools/index.ts index 41967dc..101e94b 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -24,26 +24,42 @@ import * as protocolExtensions from "./protocol-extensions.js"; import * as protocol from "./protocol.js"; import * as preflight from "./preflight.js"; import { withStructuredToolResults } from "./results.js"; +import { ALL_TOOLS, scopedServer, type RegisteredTool, type ToolFilter, type ToolGroup } from "./toolsets.js"; -// Export the initialization function for all tools -export function initialize(server: McpServer, client: OBSWebSocketClient): void { +const MODULES: ReadonlyArray<[ToolGroup, { initialize(server: McpServer, client: OBSWebSocketClient): void }]> = [ + ["general", general], + ["scenes", scenes], + ["sources", sources], + ["scene-items", sceneItems], + ["stream", streaming], + ["transitions", transitions], + ["config", config], + ["filters", filters], + ["inputs", inputs], + ["media", mediaInputs], + ["outputs", outputs], + ["record", record], + ["ui", ui], + ["protocol", protocolExtensions], // Regrouped per tool in toolsets.ts. + ["protocol", protocol], + ["record", preflight], +]; + +/** + * Registers the tools the filter includes and returns every tool with its + * group and whether it was registered. + */ +export function initialize( + server: McpServer, + client: OBSWebSocketClient, + filter: ToolFilter = ALL_TOOLS, +): RegisteredTool[] { const structuredServer = withStructuredToolResults(server); - general.initialize(structuredServer, client); - scenes.initialize(structuredServer, client); - sources.initialize(structuredServer, client); - sceneItems.initialize(structuredServer, client); - streaming.initialize(structuredServer, client); - transitions.initialize(structuredServer, client); - config.initialize(structuredServer, client); - filters.initialize(structuredServer, client); - inputs.initialize(structuredServer, client); - mediaInputs.initialize(structuredServer, client); - outputs.initialize(structuredServer, client); - record.initialize(structuredServer, client); - ui.initialize(structuredServer, client); - protocolExtensions.initialize(structuredServer, client); - protocol.initialize(structuredServer, client); - preflight.initialize(structuredServer, client); + const registry: RegisteredTool[] = []; + for (const [group, module] of MODULES) { + module.initialize(scopedServer(structuredServer, group, filter, registry), client); + } + return registry; } // Export tool modules diff --git a/src/tools/toolsets.test.ts b/src/tools/toolsets.test.ts new file mode 100644 index 0000000..2f0baf8 --- /dev/null +++ b/src/tools/toolsets.test.ts @@ -0,0 +1,92 @@ +/* + * Modified for the independent OBS MCP project in August 2026. + * See NOTICE.md and Git history for authorship and change dates. + * SPDX-License-Identifier: GPL-2.0-only + */ +import { McpServer } from "@modelcontextprotocol/server"; +import { describe, expect, it } from "vitest"; +import { OBSWebSocketClient } from "../client.js"; +import { TOOL_COUNT } from "../../test/support/tool-count.js"; +import { initialize } from "./index.js"; +import { CORE_GROUPS, TOOL_GROUPS, assertKnownTools, parseToolFilter, type ToolFilter } from "./toolsets.js"; + +function registry(filter?: ToolFilter) { + return initialize( + new McpServer({ name: "obs-mcp-test", version: "0.0.0" }), + new OBSWebSocketClient("ws://127.0.0.1:1"), + filter, + ); +} + +function registeredNames(filter: ToolFilter): string[] { + return registry(filter).filter(({ registered }) => registered).map(({ name }) => name); +} + +describe("parseToolFilter", () => { + it("registers every tool when nothing is configured", () => { + expect(parseToolFilter({})).toEqual({ groups: null, tools: new Set(), readOnly: false }); + expect(parseToolFilter({ OBS_MCP_TOOLSETS: "all,scenes" }).groups).toBeNull(); + }); + + it("expands core and accepts individual tools", () => { + const filter = parseToolFilter({ OBS_MCP_TOOLSETS: " core , stream ", OBS_MCP_TOOLS: "obs-describe-request" }); + + expect([...filter.groups ?? []].sort()).toEqual([...CORE_GROUPS, "stream"].sort()); + expect([...filter.tools]).toEqual(["obs-describe-request"]); + }); + + it("rejects unknown groups", () => { + expect(() => parseToolFilter({ OBS_MCP_TOOLSETS: "scenez" })).toThrow(/Unknown OBS_MCP_TOOLSETS entry "scenez"/); + }); + + it("reads OBS_MCP_READ_ONLY", () => { + expect(parseToolFilter({ OBS_MCP_READ_ONLY: "true" }).readOnly).toBe(true); + expect(parseToolFilter({ OBS_MCP_READ_ONLY: "0" }).readOnly).toBe(false); + }); +}); + +describe("tool registry", () => { + it("assigns every tool to a group and registers all of them by default", () => { + const tools = registry(); + + expect(tools).toHaveLength(TOOL_COUNT); + expect(tools.every(({ registered }) => registered)).toBe(true); + expect(new Set(tools.map(({ group }) => group))).toEqual(new Set(TOOL_GROUPS)); + }); + + it("registers only the chosen groups plus named tools", () => { + const names = registeredNames(parseToolFilter({ OBS_MCP_TOOLSETS: "record", OBS_MCP_TOOLS: "obs-get-scene-list" })); + + expect(names).toEqual(expect.arrayContaining([ + "obs-start-record", + "obs-stop-record", + "obs-preflight", + "obs-get-scene-list", + ])); + expect(names).not.toContain("obs-start-stream"); + expect(names).not.toContain("obs-set-current-scene"); + }); + + it("keeps the core set small", () => { + const names = registeredNames(parseToolFilter({ OBS_MCP_TOOLSETS: "core" })); + + expect(names.length).toBeLessThan(TOOL_COUNT * 0.6); + expect(names).toEqual(expect.arrayContaining(["obs-get-status", "obs-preflight", "obs-get-source-screenshot"])); + }); + + it("registers only read-only tools in read-only mode, even when named explicitly", () => { + const tools = registry(parseToolFilter({ OBS_MCP_READ_ONLY: "1", OBS_MCP_TOOLSETS: "record", OBS_MCP_TOOLS: "obs-start-record" })); + const registered = tools.filter((tool) => tool.registered); + + expect(registered.length).toBeGreaterThan(0); + expect(registered.every(({ annotations }) => annotations?.readOnlyHint === true)).toBe(true); + expect(registered.map(({ name }) => name)).not.toContain("obs-start-record"); + expect(registered.map(({ name }) => name)).toContain("obs-get-record-status"); + }); + + it("rejects unknown tool names", () => { + const filter = parseToolFilter({ OBS_MCP_TOOLS: "obs-start-recording" }); + + expect(() => assertKnownTools(filter, registry(filter))).toThrow(/obs-start-recording/); + }); +}); diff --git a/src/tools/toolsets.ts b/src/tools/toolsets.ts new file mode 100644 index 0000000..38209b2 --- /dev/null +++ b/src/tools/toolsets.ts @@ -0,0 +1,167 @@ +/* + * Modified for the independent OBS MCP project in August 2026. + * See NOTICE.md and Git history for authorship and change dates. + * SPDX-License-Identifier: GPL-2.0-only + */ +import type { McpServer, ToolAnnotations } from "@modelcontextprotocol/server"; + +export const TOOL_GROUPS = [ + "general", + "scenes", + "scene-items", + "sources", + "inputs", + "media", + "filters", + "transitions", + "record", + "stream", + "outputs", + "config", + "ui", + "protocol", +] as const; + +export type ToolGroup = typeof TOOL_GROUPS[number]; + +/** The small set recommended for recording and scene work: `OBS_MCP_TOOLSETS=core`. */ +export const CORE_GROUPS: readonly ToolGroup[] = [ + "general", + "scenes", + "scene-items", + "sources", + "inputs", + "record", +]; + +export type ToolFilter = { + /** null registers every group. */ + groups: ReadonlySet<ToolGroup> | null; + tools: ReadonlySet<string>; + readOnly: boolean; +}; + +export const ALL_TOOLS: ToolFilter = { groups: null, tools: new Set(), readOnly: false }; + +export type RegisteredTool = { + name: string; + group: ToolGroup; + annotations: ToolAnnotations | undefined; + registered: boolean; +}; + +function splitList(value: string | undefined): string[] { + return (value ?? "").split(",").map((entry) => entry.trim()).filter(Boolean); +} + +function isToolGroup(value: string): value is ToolGroup { + return (TOOL_GROUPS as readonly string[]).includes(value); +} + +/** + * Reads OBS_MCP_TOOLSETS, OBS_MCP_TOOLS, and OBS_MCP_READ_ONLY. With neither + * list set every tool is registered, matching earlier releases. Unknown group + * names throw so a typo cannot silently hide tools. + */ +export function parseToolFilter(env: NodeJS.ProcessEnv = process.env): ToolFilter { + const groupNames = splitList(env.OBS_MCP_TOOLSETS); + const tools = new Set(splitList(env.OBS_MCP_TOOLS)); + const readOnly = ["1", "true", "yes"].includes((env.OBS_MCP_READ_ONLY ?? "").trim().toLowerCase()); + + if (groupNames.length === 0 && tools.size === 0) return { groups: null, tools, readOnly }; + if (groupNames.includes("all")) return { groups: null, tools, readOnly }; + + const groups = new Set<ToolGroup>(); + for (const name of groupNames) { + if (name === "core") { + CORE_GROUPS.forEach((group) => groups.add(group)); + } else if (isToolGroup(name)) { + groups.add(name); + } else { + throw new Error( + `Unknown OBS_MCP_TOOLSETS entry "${name}". Use all, core, or: ${TOOL_GROUPS.join(", ")}`, + ); + } + } + return { groups, tools, readOnly }; +} + +export function includesTool(filter: ToolFilter, tool: Omit<RegisteredTool, "registered">): boolean { + // Read-only mode wins over explicit groups and tool names. + if (filter.readOnly && tool.annotations?.readOnlyHint !== true) return false; + if (filter.groups === null) return true; + return filter.groups.has(tool.group) || filter.tools.has(tool.name); +} + +/** Throws when OBS_MCP_TOOLS names a tool that does not exist. */ +export function assertKnownTools(filter: ToolFilter, tools: readonly RegisteredTool[]): void { + const known = new Set(tools.map(({ name }) => name)); + const unknown = [...filter.tools].filter((name) => !known.has(name)); + if (unknown.length > 0) { + throw new Error(`Unknown OBS_MCP_TOOLS entries: ${unknown.join(", ")}`); + } +} + +/** + * Tools whose module does not match their group. protocol-extensions holds + * requests added after the original modules, across several areas. + */ +const GROUP_OVERRIDES: Record<string, ToolGroup> = { + "obs-get-canvas-list": "scenes", + "obs-set-scene-name": "scenes", + "obs-get-input-audio-tracks": "inputs", + "obs-set-input-audio-tracks": "inputs", + "obs-get-input-deinterlace-field-order": "inputs", + "obs-set-input-deinterlace-field-order": "inputs", + "obs-get-input-deinterlace-mode": "inputs", + "obs-set-input-deinterlace-mode": "inputs", + "obs-get-input-property-list-items": "inputs", + "obs-press-input-property-button": "inputs", + "obs-duplicate-scene-item": "scene-items", + "obs-get-group-scene-items": "scene-items", + "obs-get-group-list": "scene-items", + "obs-get-scene-item-enabled": "scene-items", + "obs-get-scene-item-index": "scene-items", + "obs-set-scene-item-index": "scene-items", + "obs-get-scene-item-locked": "scene-items", + "obs-set-scene-item-locked": "scene-items", + "obs-get-scene-item-blend-mode": "scene-items", + "obs-set-scene-item-blend-mode": "scene-items", + "obs-get-scene-item-source": "scene-items", + "obs-get-scene-transition-override": "transitions", + "obs-set-scene-transition-override": "transitions", + "obs-get-transition-kind-list": "transitions", + "obs-get-transition-cursor": "transitions", + "obs-set-tbar-position": "transitions", +}; + +/** + * Wraps tool registration for one module: records every tool with its group + * and registers only those the filter includes. Other McpServer methods stay + * bound to the real server. + */ +export function scopedServer( + server: McpServer, + group: ToolGroup, + filter: ToolFilter, + registry: RegisteredTool[], +): McpServer { + return new Proxy(server, { + get(target, property) { + if (property === "registerTool") { + return (...registrationArgs: unknown[]) => { + const [name, config] = registrationArgs; + if (typeof name !== "string") throw new TypeError("MCP tool registration requires a name"); + const annotations = (config as { annotations?: ToolAnnotations } | undefined)?.annotations; + const tool = { name, group: GROUP_OVERRIDES[name] ?? group, annotations }; + const registered = includesTool(filter, tool); + registry.push({ ...tool, registered }); + if (!registered) return undefined; + return Reflect.apply(Reflect.get(target, property, target) as Function, target, registrationArgs); + }; + } + const value: unknown = Reflect.get(target, property, target); + return typeof value === "function" ? value.bind(target) : value; + }, + }); +} diff --git a/test/e2e/stdio.e2e.test.ts b/test/e2e/stdio.e2e.test.ts index a6272f4..ecd2218 100644 --- a/test/e2e/stdio.e2e.test.ts +++ b/test/e2e/stdio.e2e.test.ts @@ -467,6 +467,44 @@ describe("compiled stdio MCP boundary", () => { await vi.waitFor(() => expect(fake.openConnectionCount).toBe(0)); }); + it("registers only the configured tool groups in read-only mode", async () => { + const fake = await FakeOBSServer.start(); + fakeServers.push(fake); + const { client, stderr } = await connectClient(fake, "modern", { + OBS_MCP_TOOLSETS: "core", + OBS_MCP_READ_ONLY: "true", + }); + + const { tools } = await client.listTools(); + const names = tools.map(({ name }) => name); + expect(tools.length).toBeGreaterThan(0); + expect(tools.length).toBeLessThan(TOOL_COUNT); + expect(tools.every(({ annotations }) => annotations?.readOnlyHint === true)).toBe(true); + expect(names).toEqual(expect.arrayContaining(["obs-get-status", "obs-preflight"])); + expect(names).not.toContain("obs-start-record"); + expect(names).not.toContain("obs-get-stream-status"); + await vi.waitFor(() => expect(stderr.join("")).toMatch(/Registering \d+ of \d+ tools \(read-only\)/)); + }); + + it("refuses to start with an unknown tool group or tool name", async () => { + const environments: Record<string, string>[] = [ + { OBS_MCP_TOOLSETS: "scenez" }, + { OBS_MCP_TOOLS: "obs-start-recording" }, + ]; + for (const environment of environments) { + const child = spawn(process.execPath, [cliPath], { + cwd: projectRoot, + env: childEnvironment("ws://127.0.0.1:1", undefined, environment), + }); + let stderr = ""; + child.stderr.on("data", (chunk: Buffer) => { stderr += chunk.toString(); }); + const [code] = await once(child, "exit") as [number | null]; + + expect(code).toBe(1); + expect(stderr).toMatch(/scenez|obs-start-recording/); + } + }); + it("exits on stdin EOF while OBS is offline", async () => { const port = await reservePort(); const child = spawnRawChild(`ws://127.0.0.1:${port}`); From d9a91be683889b896151a2c3cc174b403620c179 Mon Sep 17 00:00:00 2001 From: Tom Main <tom@execufunction.com> Date: Tue, 22 Sep 2026 17:53:02 -0400 Subject: [PATCH 3/3] Declare output schemas generated from the pinned protocol registerObsRequestTool tools now declare outputSchema: the request's response fields from docs/protocol.json, or { message } for confirmation tools, and return matching structuredContent directly instead of re-parsing their text. Fields are optional and nullable: live OBS 32.2.2 returns undocumented nulls (GetSpecialInputs) and older versions omit newer fields. A unit test calls every schema-declaring tool against protocol-shaped fake responses; a live test calls every argument-free read-only tool against real OBS and fails on any output validation error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vjn8uBEa4mJshaQU3pFuqX --- README.md | 2 + src/tools/__snapshots__/tool-contract.json | 3177 +++++++++++++++++++- src/tools/output-schema.test.ts | 119 + src/tools/output-schema.ts | 44 + src/tools/request-tool.ts | 40 +- src/tools/tool-inventory.test.ts | 4 +- test/live/obs-live.test.ts | 19 + 7 files changed, 3333 insertions(+), 72 deletions(-) create mode 100644 src/tools/output-schema.test.ts create mode 100644 src/tools/output-schema.ts diff --git a/README.md b/README.md index 3efd7cb..6ce87ad 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,8 @@ The 156 tools are organized around the OBS protocol rather than a smaller opinio - a recording preflight (`obs-preflight`) that catches the conditions under which OBS silently refuses or botches a recording - protocol description and the guarded generic request fallback +Tools that send a single OBS request declare an `outputSchema` generated from the pinned protocol, so clients receive typed `structuredContent` with the documented response fields. Fields are optional and nullable because OBS omits newer fields in older versions and returns undocumented nulls. + Tool discovery works without an OBS connection. Calls that need OBS return an MCP error until the WebSocket connection is ready. ### Choosing tools diff --git a/src/tools/__snapshots__/tool-contract.json b/src/tools/__snapshots__/tool-contract.json index d6265f3..62a0166 100644 --- a/src/tools/__snapshots__/tool-contract.json +++ b/src/tools/__snapshots__/tool-contract.json @@ -19,6 +19,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -158,6 +172,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -205,6 +233,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -228,6 +270,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -382,6 +438,24 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "sceneItemId": { + "description": "Numeric ID of the duplicated scene item", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -396,6 +470,31 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "canvases": { + "description": "Array of canvases", + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -410,6 +509,57 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "sceneName": { + "description": "Current program scene name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "sceneUuid": { + "description": "Current program scene UUID", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "currentProgramSceneName": { + "description": "Current program scene name (Deprecated)", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "currentProgramSceneUuid": { + "description": "Current program scene UUID (Deprecated)", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -424,6 +574,94 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "transitionName": { + "description": "Name of the transition", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "transitionUuid": { + "description": "UUID of the transition", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "transitionKind": { + "description": "Kind of the transition", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "transitionFixed": { + "description": "Whether the transition uses a fixed (unconfigurable) duration", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "transitionDuration": { + "description": "Configured transition duration in milliseconds. `null` if transition is fixed", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "transitionConfigurable": { + "description": "Whether the transition supports being configured", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "transitionSettings": { + "description": "Object of settings for the transition. `null` if transition is not configurable", + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -447,6 +685,28 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "defaultFilterSettings": { + "description": "Object of default settings for the filter kind", + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -461,6 +721,27 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "sourceFilterKinds": { + "description": "Array of source filter kinds", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -475,6 +756,27 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "groups": { + "description": "Array of group names", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -503,6 +805,31 @@ }, "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "sceneItems": { + "description": "Array of scene items in the group", + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -517,6 +844,27 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "hotkeys": { + "description": "Array of hotkey names", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -540,6 +888,24 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "inputAudioBalance": { + "description": "Audio balance value from 0.0-1.0", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -563,6 +929,24 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "monitorType": { + "description": "Audio monitor type", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -586,8 +970,26 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, - "annotations": { - "readOnlyHint": true, + "outputSchema": { + "type": "object", + "properties": { + "inputAudioSyncOffset": { + "description": "Audio sync offset in milliseconds", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, + "annotations": { + "readOnlyHint": true, "destructiveHint": false, "idempotentHint": true, "openWorldHint": false @@ -610,6 +1012,28 @@ }, "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "inputAudioTracks": { + "description": "Object of audio tracks and associated enable states", + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -633,6 +1057,28 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "defaultInputSettings": { + "description": "Object of default settings for the input kind", + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -657,6 +1103,24 @@ }, "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "inputDeinterlaceFieldOrder": { + "description": "Deinterlace field order of the input", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -681,6 +1145,24 @@ }, "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "inputDeinterlaceMode": { + "description": "Deinterlace mode of the input", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -744,6 +1226,24 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "inputMuted": { + "description": "Whether the input is muted", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -775,6 +1275,31 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "propertyItems": { + "description": "Array of items in the list property", + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -798,6 +1323,39 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "inputSettings": { + "description": "Object of settings for the input", + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "inputKind": { + "description": "The kind of the input", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -821,6 +1379,35 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "inputVolumeMul": { + "description": "Volume setting in mul", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "inputVolumeDb": { + "description": "Volume setting in dB", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -835,6 +1422,24 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "savedReplayPath": { + "description": "File path", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -858,6 +1463,46 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "mediaState": { + "description": "State of the media input", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "mediaDuration": { + "description": "Total duration of the playing media in milliseconds. `null` if not playing", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "mediaCursor": { + "description": "Position of the cursor in milliseconds. `null` if not playing", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -872,6 +1517,31 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "monitors": { + "description": "a list of detected monitors with some information", + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -886,6 +1556,31 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "outputs": { + "description": "Array of outputs", + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -909,6 +1604,28 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "outputSettings": { + "description": "Output settings", + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -932,38 +1649,143 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, - "annotations": { - "readOnlyHint": true, - "destructiveHint": false, - "idempotentHint": true, - "openWorldHint": false - } - }, - { - "name": "obs-get-persistent-data", - "title": "Get Persistent Data", - "inputSchema": { + "outputSchema": { "type": "object", "properties": { - "realm": { - "type": "string", - "description": "The data realm to select. OBS_WEBSOCKET_DATA_REALM_GLOBAL or OBS_WEBSOCKET_DATA_REALM_PROFILE" + "outputActive": { + "description": "Whether the output is active", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] }, - "slotName": { - "type": "string", - "description": "The name of the slot to retrieve data from" - } - }, - "required": [ - "realm", - "slotName" - ], - "$schema": "https://json-schema.org/draft/2020-12/schema" - }, - "annotations": { - "readOnlyHint": true, - "destructiveHint": false, - "idempotentHint": true, + "outputReconnecting": { + "description": "Whether the output is reconnecting", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "outputTimecode": { + "description": "Current formatted timecode string for the output", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "outputDuration": { + "description": "Current duration in milliseconds for the output", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "outputCongestion": { + "description": "Congestion of the output", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "outputBytes": { + "description": "Number of bytes sent by the output", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "outputSkippedFrames": { + "description": "Number of frames skipped by the output's process", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "outputTotalFrames": { + "description": "Total number of frames delivered by the output's process", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + } + }, + { + "name": "obs-get-persistent-data", + "title": "Get Persistent Data", + "inputSchema": { + "type": "object", + "properties": { + "realm": { + "type": "string", + "description": "The data realm to select. OBS_WEBSOCKET_DATA_REALM_GLOBAL or OBS_WEBSOCKET_DATA_REALM_PROFILE" + }, + "slotName": { + "type": "string", + "description": "The name of the slot to retrieve data from" + } + }, + "required": [ + "realm", + "slotName" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "type": "object", + "properties": { + "slotValue": { + "description": "Value associated with the slot. `null` if not set" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, "openWorldHint": false } }, @@ -974,6 +1796,57 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "sceneName": { + "description": "Current preview scene name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "sceneUuid": { + "description": "Current preview scene UUID", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "currentPreviewSceneName": { + "description": "Current preview scene name", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "currentPreviewSceneUuid": { + "description": "Current preview scene UUID", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -988,6 +1861,38 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "currentProfileName": { + "description": "The name of the current profile", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "profiles": { + "description": "Array of all available profiles", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1016,6 +1921,35 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "parameterValue": { + "description": "Value associated with the parameter. `null` if not set and no default", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "defaultParameterValue": { + "description": "Default value associated with the parameter. `null` if no default", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1030,6 +1964,24 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "recordDirectory": { + "description": "Output directory", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1044,6 +1996,68 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "outputActive": { + "description": "Whether the output is active", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "outputPaused": { + "description": "Whether the output is paused", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "outputTimecode": { + "description": "Current formatted timecode string for the output", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "outputDuration": { + "description": "Current duration in milliseconds for the output", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "outputBytes": { + "description": "Number of bytes sent by the output", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1058,20 +2072,70 @@ "type": "object", "properties": {} }, - "annotations": { - "readOnlyHint": true, - "destructiveHint": false, - "idempotentHint": true, - "openWorldHint": false - } - }, - { - "name": "obs-get-scene-collection-list", - "title": "Get Scene Collection List", - "inputSchema": { + "outputSchema": { + "type": "object", + "properties": { + "outputActive": { + "description": "Whether the output is active", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + } + }, + { + "name": "obs-get-scene-collection-list", + "title": "Get Scene Collection List", + "inputSchema": { "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "currentSceneCollectionName": { + "description": "The name of the current scene collection", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "sceneCollections": { + "description": "Array of all available scene collections", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1109,6 +2173,24 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "sceneItemBlendMode": { + "description": "Current blend mode", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1146,6 +2228,24 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "sceneItemEnabled": { + "description": "Whether the scene item is enabled. `true` for enabled, `false` for disabled", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1188,6 +2288,24 @@ }, "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "sceneItemId": { + "description": "Numeric ID of the scene item", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1225,6 +2343,24 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "sceneItemIndex": { + "description": "Index position of the scene item", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1262,6 +2398,24 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "sceneItemLocked": { + "description": "Whether the scene item is locked. `true` for locked, `false` for unlocked", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1299,6 +2453,35 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "sourceName": { + "description": "Name of the source associated with the scene item", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "sourceUuid": { + "description": "UUID of the source associated with the scene item", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1336,6 +2519,28 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "sceneItemTransform": { + "description": "Object containing scene item transform info", + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1364,6 +2569,31 @@ }, "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "sceneItems": { + "description": "Array of scene items in the scene", + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1384,6 +2614,75 @@ }, "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "currentProgramSceneName": { + "description": "Current program scene name. Can be `null` if non-main canvas or internal state desync", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "currentProgramSceneUuid": { + "description": "Current program scene UUID. Can be `null` if non-main canvas or internal state desync", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "currentPreviewSceneName": { + "description": "Current preview scene name. `null` if not in studio mode or non-main canvas", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "currentPreviewSceneUuid": { + "description": "Current preview scene UUID. `null` if not in studio mode or non-main canvas", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "scenes": { + "description": "Array of scenes", + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1412,6 +2711,35 @@ }, "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "transitionName": { + "description": "Name of the overridden scene transition, else `null`", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "transitionDuration": { + "description": "Duration of the overridden scene transition, else `null`", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1440,6 +2768,35 @@ }, "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "videoActive": { + "description": "Whether the source is showing in Program", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "videoShowing": { + "description": "Whether the source is showing in the UI (Preview, Projector, Properties)", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1472,7 +2829,62 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, - "annotations": { + "outputSchema": { + "type": "object", + "properties": { + "filterEnabled": { + "description": "Whether the filter is enabled", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "filterIndex": { + "description": "Index of the filter in the list, beginning at 0", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "filterKind": { + "description": "The kind of filter", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "filterSettings": { + "description": "Settings object associated with the filter", + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, + "annotations": { "readOnlyHint": true, "destructiveHint": false, "idempotentHint": true, @@ -1499,6 +2911,31 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "filters": { + "description": "Array of filters", + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1554,6 +2991,25 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "mimeType": { + "type": "string" + }, + "sizeBytes": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "required": [ + "mimeType", + "sizeBytes" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1568,6 +3024,79 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "desktop1": { + "description": "Name of the Desktop Audio input", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "desktop2": { + "description": "Name of the Desktop Audio 2 input", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "mic1": { + "description": "Name of the Mic/Auxiliary Audio input", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "mic2": { + "description": "Name of the Mic/Auxiliary Audio 2 input", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "mic3": { + "description": "Name of the Mic/Auxiliary Audio 3 input", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "mic4": { + "description": "Name of the Mic/Auxiliary Audio 4 input", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1582,6 +3111,134 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "cpuUsage": { + "description": "Current CPU usage in percent", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "memoryUsage": { + "description": "Amount of memory in MB currently being used by OBS", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "availableDiskSpace": { + "description": "Available disk space on the device being used for recording storage", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "activeFps": { + "description": "Current FPS being rendered", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "averageFrameRenderTime": { + "description": "Average time in milliseconds that OBS is taking to render a frame", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "renderSkippedFrames": { + "description": "Number of frames skipped by OBS in the render thread", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "renderTotalFrames": { + "description": "Total number of frames outputted by the render thread", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "outputSkippedFrames": { + "description": "Number of frames skipped by OBS in the output thread", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "outputTotalFrames": { + "description": "Total number of frames outputted by the output thread", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "webSocketSessionIncomingMessages": { + "description": "Total number of messages received by obs-websocket from the client", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "webSocketSessionOutgoingMessages": { + "description": "Total number of messages sent by obs-websocket to the client", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1610,6 +3267,39 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "streamServiceType": { + "description": "Stream service type, like `rtmp_custom` or `rtmp_common`", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "streamServiceSettings": { + "description": "Stream service settings", + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1624,23 +3314,136 @@ "type": "object", "properties": {} }, - "annotations": { - "readOnlyHint": true, - "destructiveHint": false, - "idempotentHint": true, - "openWorldHint": false - } - }, - { - "name": "obs-get-studio-mode", - "title": "Get Studio Mode", - "inputSchema": { + "outputSchema": { "type": "object", - "properties": {} - }, - "annotations": { - "readOnlyHint": true, - "destructiveHint": false, + "properties": { + "outputActive": { + "description": "Whether the output is active", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "outputReconnecting": { + "description": "Whether the output is currently reconnecting", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "outputTimecode": { + "description": "Current formatted timecode string for the output", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "outputDuration": { + "description": "Current duration in milliseconds for the output", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "outputCongestion": { + "description": "Congestion of the output", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "outputBytes": { + "description": "Number of bytes sent by the output", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "outputSkippedFrames": { + "description": "Number of frames skipped by the output's process", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "outputTotalFrames": { + "description": "Total number of frames delivered by the output's process", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + } + }, + { + "name": "obs-get-studio-mode", + "title": "Get Studio Mode", + "inputSchema": { + "type": "object", + "properties": {} + }, + "outputSchema": { + "type": "object", + "properties": { + "studioModeEnabled": { + "description": "Whether studio mode is enabled", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, "idempotentHint": true, "openWorldHint": false } @@ -1652,6 +3455,24 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "transitionCursor": { + "description": "Cursor position, between 0.0 and 1.0", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1666,6 +3487,94 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "transitionName": { + "description": "Name of the transition", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "transitionUuid": { + "description": "UUID of the transition", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "transitionKind": { + "description": "Kind of the transition", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "transitionFixed": { + "description": "Whether the transition uses a fixed (unconfigurable) duration", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "transitionDuration": { + "description": "Configured transition duration in milliseconds. `null` if transition is fixed", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "transitionConfigurable": { + "description": "Whether the transition supports being configured", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "transitionSettings": { + "description": "Object of settings for the transition. `null` if transition is not configurable", + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1680,6 +3589,94 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "transitionName": { + "description": "Name of the transition", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "transitionUuid": { + "description": "UUID of the transition", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "transitionKind": { + "description": "Kind of the transition", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "transitionFixed": { + "description": "Whether the transition uses a fixed (unconfigurable) duration", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "transitionDuration": { + "description": "Configured transition duration in milliseconds. `null` if transition is fixed", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "transitionConfigurable": { + "description": "Whether the transition supports being configured", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "transitionSettings": { + "description": "Object of settings for the transition. `null` if transition is not configurable", + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1694,6 +3691,27 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "transitionKinds": { + "description": "Array of transition kinds", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1708,6 +3726,64 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "currentSceneTransitionName": { + "description": "Name of the current scene transition. Can be null", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "currentSceneTransitionUuid": { + "description": "UUID of the current scene transition. Can be null", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "currentSceneTransitionKind": { + "description": "Kind of the current scene transition. Can be null", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "transitions": { + "description": "Array of transitions", + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1722,6 +3798,94 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "transitionName": { + "description": "Name of the transition", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "transitionUuid": { + "description": "UUID of the transition", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "transitionKind": { + "description": "Kind of the transition", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "transitionFixed": { + "description": "Whether the transition uses a fixed (unconfigurable) duration", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "transitionDuration": { + "description": "Configured transition duration in milliseconds. `null` if transition is fixed", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "transitionConfigurable": { + "description": "Whether the transition supports being configured", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "transitionSettings": { + "description": "Object of settings for the transition. `null` if transition is not configurable", + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1736,6 +3900,96 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "obsVersion": { + "description": "Current OBS Studio version", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "obsWebSocketVersion": { + "description": "Current obs-websocket version", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "rpcVersion": { + "description": "Current latest obs-websocket RPC version", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "availableRequests": { + "description": "Array of available RPC requests for the currently negotiated RPC version", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "supportedImageFormats": { + "description": "Image formats available in `GetSourceScreenshot` and `SaveSourceScreenshot` requests.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "platform": { + "description": "Name of the platform. Usually `windows`, `macos`, or `ubuntu` (linux flavor). Not guaranteed to be any of those", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "platformDescription": { + "description": "Description of the platform, like `Windows 10 (10.0)`", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1750,6 +4004,79 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "fpsNumerator": { + "description": "Numerator of the fractional FPS value", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "fpsDenominator": { + "description": "Denominator of the fractional FPS value", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "baseWidth": { + "description": "Width of the base (canvas) resolution in pixels", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "baseHeight": { + "description": "Height of the base (canvas) resolution in pixels", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "outputWidth": { + "description": "Width of the output resolution in pixels", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "outputHeight": { + "description": "Height of the output resolution in pixels", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1764,6 +4091,24 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "outputActive": { + "description": "Whether the output is active", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {} + }, "annotations": { "readOnlyHint": true, "destructiveHint": false, @@ -1792,6 +4137,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -1950,6 +4309,20 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2009,6 +4382,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": true, @@ -2032,6 +4419,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": true, @@ -2055,6 +4456,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": true, @@ -2082,6 +4497,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": true, @@ -2119,6 +4548,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": true, @@ -2151,6 +4594,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": true, @@ -2165,6 +4622,20 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2179,6 +4650,20 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2231,6 +4716,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": true, @@ -2254,6 +4753,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2277,6 +4790,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2300,6 +4827,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2323,6 +4864,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2346,6 +4901,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2376,6 +4945,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2404,6 +4987,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2434,6 +5031,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2487,6 +5098,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2521,6 +5146,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2562,6 +5201,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2590,6 +5243,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2618,6 +5285,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2718,6 +5399,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2750,6 +5445,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2782,6 +5491,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2805,6 +5528,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2868,6 +5605,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -2918,6 +5669,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -3004,6 +5769,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -3046,6 +5825,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -3163,6 +5956,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -3215,6 +6022,20 @@ }, "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -3252,6 +6073,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -3290,6 +6125,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -3327,6 +6176,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -3404,6 +6267,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -3427,6 +6304,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -3457,6 +6348,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -3508,6 +6413,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -3600,6 +6519,20 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -3623,6 +6556,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -3702,6 +6649,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": true, @@ -3730,6 +6691,20 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": true, @@ -3744,6 +6719,20 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": true, @@ -3758,6 +6747,20 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": true, @@ -3846,6 +6849,20 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -3993,6 +7010,20 @@ ], "$schema": "https://json-schema.org/draft/2020-12/schema" }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -4007,6 +7038,20 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, @@ -4021,6 +7066,20 @@ "type": "object", "properties": {} }, + "outputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation of the completed action" + } + }, + "required": [ + "message" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, "annotations": { "readOnlyHint": false, "destructiveHint": false, diff --git a/src/tools/output-schema.test.ts b/src/tools/output-schema.test.ts new file mode 100644 index 0000000..4047074 --- /dev/null +++ b/src/tools/output-schema.test.ts @@ -0,0 +1,119 @@ +/* + * Modified for the independent OBS MCP project in August 2026. + * See NOTICE.md and Git history for authorship and change dates. + * SPDX-License-Identifier: GPL-2.0-only + */ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import { OBS_PROTOCOL_REQUESTS, type ObsProtocolField } from "../obs-protocol.js"; +import { resultText, startMcpHarness, type McpHarness } from "../../test/support/mcp-harness.js"; + +type JsonSchema = { + type?: string | string[]; + enum?: unknown[]; + properties?: Record<string, JsonSchema>; + required?: string[]; + items?: JsonSchema; + minimum?: number; + anyOf?: JsonSchema[]; + additionalProperties?: JsonSchema | boolean; +}; + +function sampleValue(field: ObsProtocolField): unknown { + switch (field.valueType) { + case "String": return "sample"; + case "Number": return 1; + case "Boolean": return true; + case "Array<String>": return ["sample"]; + case "Array<Object>": return [{ sample: 1 }]; + case "Object": return { sample: 1 }; + default: return null; + } +} + +/** Builds valid tool arguments from a JSON Schema: required properties only. */ +function sampleArgument(schema: JsonSchema): unknown { + if (schema.enum) return schema.enum[0]; + if (schema.anyOf?.[0]) return sampleArgument(schema.anyOf[0]); + const type = Array.isArray(schema.type) ? schema.type[0] : schema.type; + switch (type) { + case "string": return "sample"; + case "number": + case "integer": return schema.minimum ?? 1; + case "boolean": return true; + case "array": return []; + case "object": return Object.fromEntries( + (schema.required ?? []).map((key) => [key, sampleArgument( + schema.properties?.[key] + ?? (typeof schema.additionalProperties === "object" ? schema.additionalProperties : {}), + )]), + ); + default: return "sample"; + } +} + +let harness: McpHarness; + +beforeAll(async () => { + vi.spyOn(console, "error").mockImplementation(() => undefined); + harness = await startMcpHarness(); + for (const request of OBS_PROTOCOL_REQUESTS) { + if (request.requestType === "GetVersion") continue; // The fake answers with its advertised requests. + const response = Object.fromEntries( + request.responseFields.map((field) => [field.valueName, sampleValue(field)]), + ); + harness.fakeObs.respondWith(request.requestType, () => response); + } + harness.fakeObs.respondWith("GetSourceScreenshot", () => ({ + imageData: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + })); +}); + +afterAll(async () => { + await harness.close(); + vi.restoreAllMocks(); +}); + +describe("declared output schemas", () => { + it("are declared by every single-request tool and accept protocol-shaped responses", async () => { + const { tools } = await harness.mcpClient.listTools(); + const withSchema = tools.filter(({ outputSchema }) => outputSchema); + expect(withSchema.length).toBeGreaterThanOrEqual(110); + + const failures: string[] = []; + for (const tool of withSchema) { + const args = sampleArgument(tool.inputSchema as JsonSchema) as Record<string, unknown>; + try { + const result = await harness.call(tool.name, args); + if (result.isError) failures.push(`${tool.name}: ${resultText(result)}`); + } catch (error) { + failures.push(`${tool.name}: ${error instanceof Error ? error.message : String(error)}`); + } + } + expect(failures).toEqual([]); + }); + + it("reports a response that contradicts the protocol as a tool error", async () => { + harness.fakeObs.respondWith("GetRecordStatus", () => ({ outputActive: "yes" })); + + const result = await harness.call("obs-get-record-status"); + + expect(result.isError).toBe(true); + expect(resultText(result)).toMatch(/Output validation error.*outputActive/); + }); + + it("accepts null for fields the protocol does not document as nullable", async () => { + harness.fakeObs.respondWith("GetSpecialInputs", () => ({ desktop1: "Desktop Audio", desktop2: null })); + + const result = await harness.call("obs-get-special-inputs"); + + expect(result.isError).toBeFalsy(); + }); + + it("accepts responses from older OBS versions that omit newer fields", async () => { + harness.fakeObs.respondWith("GetSceneList", () => ({ scenes: [], currentProgramSceneName: "Main" })); + + const result = await harness.call("obs-get-scene-list"); + + expect(result.isError).toBeFalsy(); + }); +}); diff --git a/src/tools/output-schema.ts b/src/tools/output-schema.ts new file mode 100644 index 0000000..bca7920 --- /dev/null +++ b/src/tools/output-schema.ts @@ -0,0 +1,44 @@ +/* + * Modified for the independent OBS MCP project in August 2026. + * See NOTICE.md and Git history for authorship and change dates. + * SPDX-License-Identifier: GPL-2.0-only + */ +import { z } from "zod"; +import { getObsProtocolRequest, type ObsProtocolField } from "../obs-protocol.js"; + +function fieldSchema(field: ObsProtocolField): z.ZodType { + let schema: z.ZodType; + switch (field.valueType) { + case "String": schema = z.string(); break; + case "Number": schema = z.number(); break; + case "Boolean": schema = z.boolean(); break; + case "Array<String>": schema = z.array(z.string()); break; + case "Array<Object>": schema = z.array(z.record(z.string(), z.unknown())); break; + case "Object": schema = z.record(z.string(), z.unknown()); break; + default: return z.unknown().optional().describe(field.valueDescription); + } + // Nullable and optional: the protocol documents nulls only in prose and not + // always (GetSpecialInputs returns null for unset devices), and older OBS + // versions omit fields added later. A stricter schema would turn a working + // call into an output validation error. + return schema.nullable().optional().describe(field.valueDescription); +} + +/** + * The structuredContent shape of a tool that returns an OBS response as-is, + * derived from the pinned protocol. Extra fields are allowed so newer OBS + * versions that add response fields still validate. + */ +export function responseOutputSchema(requestType: string): z.ZodObject | undefined { + const request = getObsProtocolRequest(requestType); + if (!request) return undefined; + const shape = Object.fromEntries( + request.responseFields.map((field) => [field.valueName, fieldSchema(field)]), + ); + return z.looseObject(shape); +} + +/** The structuredContent shape of a tool that answers with a confirmation. */ +export const MESSAGE_OUTPUT_SCHEMA = z.object({ + message: z.string().describe("Confirmation of the completed action"), +}); diff --git a/src/tools/request-tool.ts b/src/tools/request-tool.ts index d213866..9819bf8 100644 --- a/src/tools/request-tool.ts +++ b/src/tools/request-tool.ts @@ -6,6 +6,7 @@ import type { McpServer, ToolAnnotations } from "@modelcontextprotocol/server"; import type { ZodObject } from "zod"; import type { OBSWebSocketClient } from "../client.js"; +import { MESSAGE_OUTPUT_SCHEMA, responseOutputSchema } from "./output-schema.js"; export const READ_ONLY_TOOL = { readOnlyHint: true, @@ -49,17 +50,23 @@ type RequestToolDefinition = { guard?: (client: OBSWebSocketClient, args: Record<string, unknown>) => Promise<string | undefined>; }; -function formatResponse( +function isObject(value: unknown): value is Record<string, unknown> { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function returnsMessage(definition: RequestToolDefinition): boolean { + return definition.successMessage !== undefined || definition.responseMode === "success"; +} + +function structuredResponse( definition: RequestToolDefinition, response: unknown, requestData: Record<string, unknown>, -): string { - if (definition.successMessage) return definition.successMessage(requestData); - if (definition.responseMode === "success") { - return `${definition.title} completed successfully`; - } - - return JSON.stringify(response, null, 2); +): Record<string, unknown> { + if (!returnsMessage(definition)) return isObject(response) ? response : {}; + return { + message: definition.successMessage?.(requestData) ?? `${definition.title} completed successfully`, + }; } async function executeRequest( @@ -76,8 +83,15 @@ async function executeRequest( }; } const response = await client.sendRequest(definition.requestType, requestData); + const structuredContent = structuredResponse(definition, response, requestData ?? {}); return { - content: [{ type: "text" as const, text: formatResponse(definition, response, requestData ?? {}) }], + content: [{ + type: "text" as const, + text: returnsMessage(definition) + ? String(structuredContent.message) + : JSON.stringify(structuredContent, null, 2), + }], + structuredContent, }; } catch (error) { return { @@ -96,11 +110,15 @@ export function registerObsRequestTool( definition: RequestToolDefinition, ): void { const { name, title, description, inputSchema, annotations } = definition; + const outputSchema = returnsMessage(definition) + ? MESSAGE_OUTPUT_SCHEMA + : responseOutputSchema(definition.requestType); + const outputConfig = outputSchema ? { outputSchema } : {}; if (inputSchema) { server.registerTool( name, - { title, description, inputSchema, annotations }, + { title, description, inputSchema, annotations, ...outputConfig }, async (args) => executeRequest( client, definition, @@ -112,7 +130,7 @@ export function registerObsRequestTool( server.registerTool( name, - { title, description, annotations }, + { title, description, annotations, ...outputConfig }, async () => executeRequest(client, definition), ); } diff --git a/src/tools/tool-inventory.test.ts b/src/tools/tool-inventory.test.ts index 84b3de0..469ea10 100644 --- a/src/tools/tool-inventory.test.ts +++ b/src/tools/tool-inventory.test.ts @@ -16,7 +16,7 @@ import { initialize } from "./index.js"; const DESTRUCTIVE_NAME = /^obs-(remove-.+|stop-(stream|record|output|virtual-cam|replay-buffer)|toggle-(stream|record|output|virtual-cam|replay-buffer))$/; describe("tool contract snapshot", () => { - it("keeps tool names, input schemas, and annotations stable", async () => { + it("keeps tool names, input and output schemas, and annotations stable", async () => { const server = new McpServer({ name: "obs-mcp-test", version: "0.0.0" }); const client = new Client({ name: "obs-mcp-test-client", version: "0.0.0" }); initialize(server, new OBSWebSocketClient("ws://127.0.0.1:1")); @@ -26,7 +26,7 @@ describe("tool contract snapshot", () => { try { const { tools } = await client.listTools(); const contract = tools - .map(({ name, title, inputSchema, annotations }) => ({ name, title, inputSchema, annotations })) + .map(({ name, title, inputSchema, outputSchema, annotations }) => ({ name, title, inputSchema, outputSchema, annotations })) .sort((a, b) => a.name.localeCompare(b.name)); const unsafe = contract .filter(({ name, annotations }) => DESTRUCTIVE_NAME.test(name) && !annotations?.destructiveHint) diff --git a/test/live/obs-live.test.ts b/test/live/obs-live.test.ts index 7001fe2..8d0590d 100644 --- a/test/live/obs-live.test.ts +++ b/test/live/obs-live.test.ts @@ -116,6 +116,25 @@ describe.skipIf(!enabled)("live OBS through compiled MCP stdio", () => { } }); + it("returns results that match every read-only tool's declared output schema", async () => { + if (!client) throw new Error("Live MCP client is not connected"); + const { tools } = await client.listTools(); + const candidates = tools.filter(({ annotations, outputSchema, inputSchema }) => ( + annotations?.readOnlyHint === true + && outputSchema !== undefined + && (inputSchema.required ?? []).length === 0 + )); + expect(candidates.length).toBeGreaterThan(20); + + const mismatches: string[] = []; + for (const tool of candidates) { + const result = await client.callTool({ name: tool.name, arguments: {} }); + const text = textContent(result as CallToolResult); + if (/Output validation error/.test(text)) mismatches.push(`${tool.name}: ${text}`); + } + expect(mismatches).toEqual([]); + }, 60_000); + it.skipIf(!mutationsEnabled)("creates and removes one scene in the approved collection", async () => { const expectedCollection = process.env.OBS_MCP_LIVE_SCENE_COLLECTION; if (!expectedCollection) {