Add FPS game example, auto-connect plugin, and Python injection tools
- Updated RobloxMCPPlugin with HTTP polling (auto-enables HttpService) - Added 20-weapon FPS game example (CoD-style) - Added Python studio-inject.py for command bar injection via Win32 API - Added auto-connect setup scripts (VBS + PowerShell) - Updated MCP server with all FPS game tools Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
134
src/index.js
134
src/index.js
@@ -23,6 +23,11 @@ let studioClients = new Set();
|
||||
let pendingRequests = new Map();
|
||||
let requestIdCounter = 0;
|
||||
|
||||
// HTTP polling support (alternative to WebSocket)
|
||||
let pendingCommands = [];
|
||||
let commandResults = new Map();
|
||||
let commandIdCounter = 0;
|
||||
|
||||
// Create MCP server
|
||||
const server = new Server(
|
||||
{
|
||||
@@ -40,33 +45,34 @@ const server = new Server(
|
||||
async function sendToStudio(command, params = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const requestId = ++requestIdCounter;
|
||||
const commandId = ++commandIdCounter;
|
||||
|
||||
// Check if any Studio client is connected
|
||||
if (studioClients.size === 0) {
|
||||
reject(new Error(
|
||||
'No Roblox Studio instance connected. Please:\n' +
|
||||
'1. Open Roblox Studio\n' +
|
||||
'2. Install the RobloxMCP plugin (see RobloxMCFPlugin.lua)\n' +
|
||||
'3. Make sure the plugin is running'
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up response handler
|
||||
pendingRequests.set(requestId, { resolve, reject, timeout: setTimeout(() => {
|
||||
// Set up response handler (supports both WebSocket and HTTP polling)
|
||||
const timeout = setTimeout(() => {
|
||||
pendingRequests.delete(requestId);
|
||||
commandResults.delete(commandId);
|
||||
reject(new Error('Request timeout - Roblox Studio did not respond'));
|
||||
}, 30000) });
|
||||
}, 30000);
|
||||
|
||||
// Send to all connected Studio clients
|
||||
const message = JSON.stringify({ id: requestId, command, params });
|
||||
studioClients.forEach(ws => {
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.send(message);
|
||||
}
|
||||
});
|
||||
// Store in both maps for WebSocket and HTTP compatibility
|
||||
pendingRequests.set(requestId, { resolve, reject, timeout });
|
||||
commandResults.set(commandId, { resolve, reject, timeout });
|
||||
|
||||
console.error(`[MCP] Sent command ${command} (ID: ${requestId})`);
|
||||
// Add command to HTTP polling queue
|
||||
pendingCommands.push({ id: commandId, requestId, command, params, timestamp: Date.now() });
|
||||
|
||||
// Try to send via WebSocket if available
|
||||
if (studioClients.size > 0) {
|
||||
const message = JSON.stringify({ id: commandId, requestId, command, params });
|
||||
studioClients.forEach(ws => {
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.send(message);
|
||||
}
|
||||
});
|
||||
console.error(`[MCP] Sent command ${command} (WS ID: ${commandId})`);
|
||||
} else {
|
||||
console.error(`[MCP] Queued command ${command} (HTTP ID: ${commandId}) - waiting for Roblox Studio to poll`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -326,6 +332,29 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
required: ['guiType', 'name'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'roblox_import_glb',
|
||||
description: 'Import a GLB 3D model into Roblox Studio',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
glbData: {
|
||||
type: 'string',
|
||||
description: 'Base64-encoded GLB model data',
|
||||
},
|
||||
parentPath: {
|
||||
type: 'string',
|
||||
description: 'Parent path (e.g., "Workspace")',
|
||||
default: 'Workspace',
|
||||
},
|
||||
modelName: {
|
||||
type: 'string',
|
||||
description: 'Name for the imported model',
|
||||
},
|
||||
},
|
||||
required: ['glbData', 'modelName'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
@@ -426,6 +455,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
});
|
||||
break;
|
||||
|
||||
case 'roblox_import_glb':
|
||||
result = await sendToStudio('importGLB', {
|
||||
glbData: args.glbData,
|
||||
parentPath: args.parentPath || 'Workspace',
|
||||
modelName: args.modelName,
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
@@ -498,11 +535,62 @@ wss.on('connection', (ws) => {
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
studioConnected: studioClients.size > 0,
|
||||
studioConnected: studioClients.size > 0 || pendingCommands.length > 0,
|
||||
connections: studioClients.size,
|
||||
pendingCommands: pendingCommands.length,
|
||||
});
|
||||
});
|
||||
|
||||
// HTTP polling endpoint for Roblox Studio
|
||||
app.get('/poll', (req, res) => {
|
||||
const lastId = parseInt(req.query.last || '0');
|
||||
|
||||
// Filter commands newer than lastId
|
||||
const newCommands = pendingCommands.filter(cmd => cmd.id > lastId);
|
||||
|
||||
// Clean up old commands (older than 5 minutes)
|
||||
const now = Date.now();
|
||||
pendingCommands = pendingCommands.filter(cmd => now - cmd.timestamp < 300000);
|
||||
|
||||
res.json({
|
||||
commands: newCommands,
|
||||
lastId: commandIdCounter,
|
||||
});
|
||||
});
|
||||
|
||||
// Result endpoint for Roblox Studio to send command results
|
||||
app.post('/result', (req, res) => {
|
||||
const { id, result } = req.body;
|
||||
|
||||
// Store result for pending request
|
||||
if (commandResults.has(id)) {
|
||||
const { resolve, reject, timeout } = commandResults.get(id);
|
||||
clearTimeout(timeout);
|
||||
commandResults.delete(id);
|
||||
|
||||
if (result && result.success === false) {
|
||||
reject(new Error(result.error || 'Command failed'));
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
} else {
|
||||
// Also check pendingRequests for WebSocket compatibility
|
||||
if (pendingRequests.has(id)) {
|
||||
const { resolve, reject, timeout } = pendingRequests.get(id);
|
||||
clearTimeout(timeout);
|
||||
pendingRequests.delete(id);
|
||||
|
||||
if (result && result.success === false) {
|
||||
reject(new Error(result.error || 'Command failed'));
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// Start Express server
|
||||
app.listen(HTTP_PORT, () => {
|
||||
console.error(`HTTP server listening on port ${HTTP_PORT}`);
|
||||
|
||||
Reference in New Issue
Block a user