index.ts•28.9 kB
#!/usr/bin/env node
/**
* Roblox Studio MCP Server
*
* This server provides Model Context Protocol (MCP) tools for interacting with Roblox Studio.
* It allows AI assistants to access Studio data, scripts, and objects through a bridge plugin.
*
* Usage:
* npx robloxstudio-mcp
*
* Or add to your MCP configuration:
* "robloxstudio": {
* "command": "npx",
* "args": ["-y", "robloxstudio-mcp"]
* }
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { createHttpServer } from './http-server.js';
import { RobloxStudioTools } from './tools/index.js';
import { BridgeService } from './bridge-service.js';
class RobloxStudioMCPServer {
private server: Server;
private tools: RobloxStudioTools;
private bridge: BridgeService;
constructor() {
this.server = new Server(
{
name: 'robloxstudio-mcp',
version: '1.6.0',
},
{
capabilities: {
tools: {},
},
}
);
this.bridge = new BridgeService();
this.tools = new RobloxStudioTools(this.bridge);
this.setupToolHandlers();
}
private setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
// File System Tools
{
name: 'get_file_tree',
description: 'Get complete hierarchy of the Roblox Studio project with script types, models, and folders',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Optional path to start from (defaults to workspace root)',
default: ''
}
}
}
},
{
name: 'search_files',
description: 'Find files by name, type, or content patterns',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query (name, type, or content pattern)'
},
searchType: {
type: 'string',
enum: ['name', 'type', 'content'],
description: 'Type of search to perform',
default: 'name'
}
},
required: ['query']
}
},
// Studio Context Tools
{
name: 'get_place_info',
description: 'Get place ID, name, and game settings',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'get_services',
description: 'Get available Roblox services and their children',
inputSchema: {
type: 'object',
properties: {
serviceName: {
type: 'string',
description: 'Optional specific service name to query'
}
}
}
},
{
name: 'search_objects',
description: 'Find instances by name, class, or properties',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query'
},
searchType: {
type: 'string',
enum: ['name', 'class', 'property'],
description: 'Type of search to perform',
default: 'name'
},
propertyName: {
type: 'string',
description: 'Property name when searchType is "property"'
}
},
required: ['query']
}
},
// Property & Instance Tools
{
name: 'get_instance_properties',
description: 'Get all properties of a specific instance',
inputSchema: {
type: 'object',
properties: {
instancePath: {
type: 'string',
description: 'Path to the instance'
}
},
required: ['instancePath']
}
},
{
name: 'get_instance_children',
description: 'Get child objects and their types',
inputSchema: {
type: 'object',
properties: {
instancePath: {
type: 'string',
description: 'Path to the parent instance'
}
},
required: ['instancePath']
}
},
{
name: 'search_by_property',
description: 'Find objects with specific property values',
inputSchema: {
type: 'object',
properties: {
propertyName: {
type: 'string',
description: 'Name of the property to search'
},
propertyValue: {
type: 'string',
description: 'Value to search for'
}
},
required: ['propertyName', 'propertyValue']
}
},
{
name: 'get_class_info',
description: 'Get available properties/methods for Roblox classes',
inputSchema: {
type: 'object',
properties: {
className: {
type: 'string',
description: 'Roblox class name'
}
},
required: ['className']
}
},
// Project Tools
{
name: 'get_project_structure',
description: 'Get complete game hierarchy. IMPORTANT: Use maxDepth parameter (default: 3) to explore deeper levels of the hierarchy. Set higher values like 5-10 for comprehensive exploration',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Optional path to start from (defaults to workspace root)',
default: ''
},
maxDepth: {
type: 'number',
description: 'Maximum depth to traverse (default: 3). RECOMMENDED: Use 5-10 for thorough exploration. Higher values provide more complete structure',
default: 3
},
scriptsOnly: {
type: 'boolean',
description: 'Show only scripts and script containers',
default: false
}
}
}
},
// Property Modification Tools
{
name: 'set_property',
description: 'Set a property on any Roblox instance',
inputSchema: {
type: 'object',
properties: {
instancePath: {
type: 'string',
description: 'Path to the instance (e.g., "game.Workspace.Part")'
},
propertyName: {
type: 'string',
description: 'Name of the property to set'
},
propertyValue: {
description: 'Value to set the property to (any type)'
}
},
required: ['instancePath', 'propertyName', 'propertyValue']
}
},
{
name: 'mass_set_property',
description: 'Set the same property on multiple instances at once',
inputSchema: {
type: 'object',
properties: {
paths: {
type: 'array',
items: { type: 'string' },
description: 'Array of instance paths to modify'
},
propertyName: {
type: 'string',
description: 'Name of the property to set'
},
propertyValue: {
description: 'Value to set the property to (any type)'
}
},
required: ['paths', 'propertyName', 'propertyValue']
}
},
{
name: 'mass_get_property',
description: 'Get the same property from multiple instances at once',
inputSchema: {
type: 'object',
properties: {
paths: {
type: 'array',
items: { type: 'string' },
description: 'Array of instance paths to read from'
},
propertyName: {
type: 'string',
description: 'Name of the property to get'
}
},
required: ['paths', 'propertyName']
}
},
// Object Creation/Deletion Tools
{
name: 'create_object',
description: 'Create a new Roblox object instance (basic, without properties)',
inputSchema: {
type: 'object',
properties: {
className: {
type: 'string',
description: 'Roblox class name (e.g., "Part", "Script", "Folder")'
},
parent: {
type: 'string',
description: 'Path to the parent instance (e.g., "game.Workspace")'
},
name: {
type: 'string',
description: 'Optional name for the new object'
}
},
required: ['className', 'parent']
}
},
{
name: 'create_object_with_properties',
description: 'Create a new Roblox object instance with initial properties',
inputSchema: {
type: 'object',
properties: {
className: {
type: 'string',
description: 'Roblox class name (e.g., "Part", "Script", "Folder")'
},
parent: {
type: 'string',
description: 'Path to the parent instance (e.g., "game.Workspace")'
},
name: {
type: 'string',
description: 'Optional name for the new object'
},
properties: {
type: 'object',
description: 'Properties to set on creation'
}
},
required: ['className', 'parent']
}
},
{
name: 'mass_create_objects',
description: 'Create multiple objects at once (basic, without properties)',
inputSchema: {
type: 'object',
properties: {
objects: {
type: 'array',
items: {
type: 'object',
properties: {
className: {
type: 'string',
description: 'Roblox class name'
},
parent: {
type: 'string',
description: 'Path to the parent instance'
},
name: {
type: 'string',
description: 'Optional name for the object'
}
},
required: ['className', 'parent']
},
description: 'Array of objects to create'
}
},
required: ['objects']
}
},
{
name: 'mass_create_objects_with_properties',
description: 'Create multiple objects at once with initial properties',
inputSchema: {
type: 'object',
properties: {
objects: {
type: 'array',
items: {
type: 'object',
properties: {
className: {
type: 'string',
description: 'Roblox class name'
},
parent: {
type: 'string',
description: 'Path to the parent instance'
},
name: {
type: 'string',
description: 'Optional name for the object'
},
properties: {
type: 'object',
description: 'Properties to set on creation'
}
},
required: ['className', 'parent']
},
description: 'Array of objects to create with properties'
}
},
required: ['objects']
}
},
{
name: 'delete_object',
description: 'Delete a Roblox object instance',
inputSchema: {
type: 'object',
properties: {
instancePath: {
type: 'string',
description: 'Path to the instance to delete'
}
},
required: ['instancePath']
}
},
// Smart Duplication Tools
{
name: 'smart_duplicate',
description: 'Smart duplication with automatic naming, positioning, and property variations',
inputSchema: {
type: 'object',
properties: {
instancePath: {
type: 'string',
description: 'Path to the instance to duplicate'
},
count: {
type: 'number',
description: 'Number of duplicates to create'
},
options: {
type: 'object',
properties: {
namePattern: {
type: 'string',
description: 'Name pattern with {n} placeholder (e.g., "Button{n}")'
},
positionOffset: {
type: 'array',
items: { type: 'number' },
minItems: 3,
maxItems: 3,
description: 'X, Y, Z offset per duplicate'
},
rotationOffset: {
type: 'array',
items: { type: 'number' },
minItems: 3,
maxItems: 3,
description: 'X, Y, Z rotation offset per duplicate'
},
scaleOffset: {
type: 'array',
items: { type: 'number' },
minItems: 3,
maxItems: 3,
description: 'X, Y, Z scale multiplier per duplicate'
},
propertyVariations: {
type: 'object',
description: 'Property name to array of values'
},
targetParents: {
type: 'array',
items: { type: 'string' },
description: 'Different parent for each duplicate'
}
}
}
},
required: ['instancePath', 'count']
}
},
{
name: 'mass_duplicate',
description: 'Perform multiple smart duplications at once',
inputSchema: {
type: 'object',
properties: {
duplications: {
type: 'array',
items: {
type: 'object',
properties: {
instancePath: {
type: 'string',
description: 'Path to the instance to duplicate'
},
count: {
type: 'number',
description: 'Number of duplicates to create'
},
options: {
type: 'object',
properties: {
namePattern: {
type: 'string',
description: 'Name pattern with {n} placeholder'
},
positionOffset: {
type: 'array',
items: { type: 'number' },
minItems: 3,
maxItems: 3,
description: 'X, Y, Z offset per duplicate'
},
rotationOffset: {
type: 'array',
items: { type: 'number' },
minItems: 3,
maxItems: 3,
description: 'X, Y, Z rotation offset per duplicate'
},
scaleOffset: {
type: 'array',
items: { type: 'number' },
minItems: 3,
maxItems: 3,
description: 'X, Y, Z scale multiplier per duplicate'
},
propertyVariations: {
type: 'object',
description: 'Property name to array of values'
},
targetParents: {
type: 'array',
items: { type: 'string' },
description: 'Different parent for each duplicate'
}
}
}
},
required: ['instancePath', 'count']
},
description: 'Array of duplication operations'
}
},
required: ['duplications']
}
},
// Calculated Property Tools
{
name: 'set_calculated_property',
description: 'Set properties using mathematical formulas and variables',
inputSchema: {
type: 'object',
properties: {
paths: {
type: 'array',
items: { type: 'string' },
description: 'Array of instance paths to modify'
},
propertyName: {
type: 'string',
description: 'Name of the property to set'
},
formula: {
type: 'string',
description: 'Mathematical formula (e.g., "Position.magnitude * 2", "index * 50")'
},
variables: {
type: 'object',
description: 'Additional variables for the formula'
}
},
required: ['paths', 'propertyName', 'formula']
}
},
// Relative Property Tools
{
name: 'set_relative_property',
description: 'Modify properties relative to their current values',
inputSchema: {
type: 'object',
properties: {
paths: {
type: 'array',
items: { type: 'string' },
description: 'Array of instance paths to modify'
},
propertyName: {
type: 'string',
description: 'Name of the property to modify'
},
operation: {
type: 'string',
enum: ['add', 'multiply', 'divide', 'subtract', 'power'],
description: 'Mathematical operation to perform'
},
value: {
description: 'Value to use in the operation'
},
component: {
type: 'string',
enum: ['X', 'Y', 'Z'],
description: 'Specific component for Vector3/UDim2 properties'
}
},
required: ['paths', 'propertyName', 'operation', 'value']
}
},
// Script Management Tools
{
name: 'get_script_source',
description: 'Get the source code of a script object (LocalScript, Script, or ModuleScript)',
inputSchema: {
type: 'object',
properties: {
instancePath: {
type: 'string',
description: 'Path to the script instance (e.g., "game.ServerScriptService.MainScript")'
}
},
required: ['instancePath']
}
},
{
name: 'set_script_source',
description: 'Safely set the source code of a script object without using loadstring (Studio only)',
inputSchema: {
type: 'object',
properties: {
instancePath: {
type: 'string',
description: 'Path to the script instance (e.g., "game.ServerScriptService.MainScript")'
},
source: {
type: 'string',
description: 'New source code for the script'
}
},
required: ['instancePath', 'source']
}
}
]
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
// File System Tools
case 'get_file_tree':
return await this.tools.getFileTree((args as any)?.path || '');
case 'search_files':
return await this.tools.searchFiles((args as any)?.query as string, (args as any)?.searchType || 'name');
// Studio Context Tools
case 'get_place_info':
return await this.tools.getPlaceInfo();
case 'get_services':
return await this.tools.getServices((args as any)?.serviceName);
case 'search_objects':
return await this.tools.searchObjects((args as any)?.query as string, (args as any)?.searchType || 'name', (args as any)?.propertyName);
// Property & Instance Tools
case 'get_instance_properties':
return await this.tools.getInstanceProperties((args as any)?.instancePath as string);
case 'get_instance_children':
return await this.tools.getInstanceChildren((args as any)?.instancePath as string);
case 'search_by_property':
return await this.tools.searchByProperty((args as any)?.propertyName as string, (args as any)?.propertyValue as string);
case 'get_class_info':
return await this.tools.getClassInfo((args as any)?.className as string);
// Project Tools
case 'get_project_structure':
return await this.tools.getProjectStructure((args as any)?.path, (args as any)?.maxDepth, (args as any)?.scriptsOnly);
// Property Modification Tools
case 'set_property':
return await this.tools.setProperty((args as any)?.instancePath as string, (args as any)?.propertyName as string, (args as any)?.propertyValue);
// Mass Property Tools
case 'mass_set_property':
return await this.tools.massSetProperty((args as any)?.paths as string[], (args as any)?.propertyName as string, (args as any)?.propertyValue);
case 'mass_get_property':
return await this.tools.massGetProperty((args as any)?.paths as string[], (args as any)?.propertyName as string);
// Object Creation/Deletion Tools
case 'create_object':
return await this.tools.createObject((args as any)?.className as string, (args as any)?.parent as string, (args as any)?.name);
case 'create_object_with_properties':
return await this.tools.createObjectWithProperties((args as any)?.className as string, (args as any)?.parent as string, (args as any)?.name, (args as any)?.properties);
case 'mass_create_objects':
return await this.tools.massCreateObjects((args as any)?.objects);
case 'mass_create_objects_with_properties':
return await this.tools.massCreateObjectsWithProperties((args as any)?.objects);
case 'delete_object':
return await this.tools.deleteObject((args as any)?.instancePath as string);
// Smart Duplication Tools
case 'smart_duplicate':
return await this.tools.smartDuplicate((args as any)?.instancePath as string, (args as any)?.count as number, (args as any)?.options);
case 'mass_duplicate':
return await this.tools.massDuplicate((args as any)?.duplications);
// Calculated Property Tools
case 'set_calculated_property':
return await this.tools.setCalculatedProperty((args as any)?.paths as string[], (args as any)?.propertyName as string, (args as any)?.formula as string, (args as any)?.variables);
// Relative Property Tools
case 'set_relative_property':
return await this.tools.setRelativeProperty((args as any)?.paths as string[], (args as any)?.propertyName as string, (args as any)?.operation, (args as any)?.value, (args as any)?.component);
// Script Management Tools
case 'get_script_source':
return await this.tools.getScriptSource((args as any)?.instancePath as string);
case 'set_script_source':
return await this.tools.setScriptSource((args as any)?.instancePath as string, (args as any)?.source as string);
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${name}`
);
}
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Tool execution failed: ${error instanceof Error ? error.message : String(error)}`
);
}
});
}
async run() {
const port = process.env.ROBLOX_STUDIO_PORT ? parseInt(process.env.ROBLOX_STUDIO_PORT) : 3002;
const host = process.env.ROBLOX_STUDIO_HOST || '0.0.0.0';
const httpServer = createHttpServer(this.tools, this.bridge);
await new Promise<void>((resolve) => {
httpServer.listen(port, host, () => {
console.error(`HTTP server listening on ${host}:${port} for Studio plugin`);
resolve();
});
});
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Roblox Studio MCP server running on stdio');
(httpServer as any).setMCPServerActive(true);
console.error('MCP server marked as active');
console.error('Waiting for Studio plugin to connect...');
setInterval(() => {
const pluginConnected = (httpServer as any).isPluginConnected();
const mcpActive = (httpServer as any).isMCPServerActive();
if (pluginConnected && mcpActive) {
} else if (pluginConnected && !mcpActive) {
console.error('Studio plugin connected, but MCP server inactive');
} else if (!pluginConnected && mcpActive) {
console.error('MCP server active, waiting for Studio plugin...');
} else {
console.error('Waiting for connections...');
}
}, 5000);
setInterval(() => {
this.bridge.cleanupOldRequests();
}, 5000);
}
}
const server = new RobloxStudioMCPServer();
server.run().catch((error) => {
console.error('Server failed to start:', error);
process.exit(1);
});