rbxstudio-mcp 2.3.2 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -14
- package/dist/__tests__/bridge-service.test.js +25 -13
- package/dist/__tests__/bridge-service.test.js.map +1 -1
- package/dist/__tests__/bridge-session.test.d.ts +2 -0
- package/dist/__tests__/bridge-session.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-session.test.js +171 -0
- package/dist/__tests__/bridge-session.test.js.map +1 -0
- package/dist/__tests__/chunker.test.d.ts +2 -0
- package/dist/__tests__/chunker.test.d.ts.map +1 -0
- package/dist/__tests__/chunker.test.js +201 -0
- package/dist/__tests__/chunker.test.js.map +1 -0
- package/dist/__tests__/docs-core.test.d.ts +2 -0
- package/dist/__tests__/docs-core.test.d.ts.map +1 -0
- package/dist/__tests__/docs-core.test.js +137 -0
- package/dist/__tests__/docs-core.test.js.map +1 -0
- package/dist/__tests__/docs-fetcher.test.d.ts +2 -0
- package/dist/__tests__/docs-fetcher.test.d.ts.map +1 -0
- package/dist/__tests__/docs-fetcher.test.js +173 -0
- package/dist/__tests__/docs-fetcher.test.js.map +1 -0
- package/dist/__tests__/helpers.d.ts +8 -0
- package/dist/__tests__/helpers.d.ts.map +1 -0
- package/dist/__tests__/helpers.js +23 -0
- package/dist/__tests__/helpers.js.map +1 -0
- package/dist/__tests__/http-routes.test.d.ts +2 -0
- package/dist/__tests__/http-routes.test.d.ts.map +1 -0
- package/dist/__tests__/http-routes.test.js +233 -0
- package/dist/__tests__/http-routes.test.js.map +1 -0
- package/dist/__tests__/http-server.test.js +13 -6
- package/dist/__tests__/http-server.test.js.map +1 -1
- package/dist/__tests__/integration.test.js +9 -4
- package/dist/__tests__/integration.test.js.map +1 -1
- package/dist/__tests__/semantic-search.test.d.ts +2 -0
- package/dist/__tests__/semantic-search.test.d.ts.map +1 -0
- package/dist/__tests__/semantic-search.test.js +202 -0
- package/dist/__tests__/semantic-search.test.js.map +1 -0
- package/dist/__tests__/smoke.test.js +7 -3
- package/dist/__tests__/smoke.test.js.map +1 -1
- package/dist/__tests__/studio-client.test.d.ts +2 -0
- package/dist/__tests__/studio-client.test.d.ts.map +1 -0
- package/dist/__tests__/studio-client.test.js +25 -0
- package/dist/__tests__/studio-client.test.js.map +1 -0
- package/dist/__tests__/tool-nudges.test.d.ts +2 -0
- package/dist/__tests__/tool-nudges.test.d.ts.map +1 -0
- package/dist/__tests__/tool-nudges.test.js +60 -0
- package/dist/__tests__/tool-nudges.test.js.map +1 -0
- package/dist/__tests__/tool-registry.test.d.ts +2 -0
- package/dist/__tests__/tool-registry.test.d.ts.map +1 -0
- package/dist/__tests__/tool-registry.test.js +365 -0
- package/dist/__tests__/tool-registry.test.js.map +1 -0
- package/dist/__tests__/tools-bridge.test.d.ts +2 -0
- package/dist/__tests__/tools-bridge.test.d.ts.map +1 -0
- package/dist/__tests__/tools-bridge.test.js +396 -0
- package/dist/__tests__/tools-bridge.test.js.map +1 -0
- package/dist/__tests__/tools-docs.test.d.ts +2 -0
- package/dist/__tests__/tools-docs.test.d.ts.map +1 -0
- package/dist/__tests__/tools-docs.test.js +112 -0
- package/dist/__tests__/tools-docs.test.js.map +1 -0
- package/dist/__tests__/tools-guards.test.d.ts +2 -0
- package/dist/__tests__/tools-guards.test.d.ts.map +1 -0
- package/dist/__tests__/tools-guards.test.js +131 -0
- package/dist/__tests__/tools-guards.test.js.map +1 -0
- package/dist/__tests__/tools-runtime.test.d.ts +2 -0
- package/dist/__tests__/tools-runtime.test.d.ts.map +1 -0
- package/dist/__tests__/tools-runtime.test.js +214 -0
- package/dist/__tests__/tools-runtime.test.js.map +1 -0
- package/dist/__tests__/tools-visual.test.d.ts +2 -0
- package/dist/__tests__/tools-visual.test.d.ts.map +1 -0
- package/dist/__tests__/tools-visual.test.js +149 -0
- package/dist/__tests__/tools-visual.test.js.map +1 -0
- package/dist/bridge-service.d.ts +99 -12
- package/dist/bridge-service.d.ts.map +1 -1
- package/dist/bridge-service.js +238 -21
- package/dist/bridge-service.js.map +1 -1
- package/dist/docs/cache.d.ts +50 -0
- package/dist/docs/cache.d.ts.map +1 -0
- package/dist/docs/cache.js +123 -0
- package/dist/docs/cache.js.map +1 -0
- package/dist/docs/embeddings/chunker.d.ts +120 -0
- package/dist/docs/embeddings/chunker.d.ts.map +1 -0
- package/dist/docs/embeddings/chunker.js +395 -0
- package/dist/docs/embeddings/chunker.js.map +1 -0
- package/dist/docs/embeddings/embedder.d.ts +41 -0
- package/dist/docs/embeddings/embedder.d.ts.map +1 -0
- package/dist/docs/embeddings/embedder.js +113 -0
- package/dist/docs/embeddings/embedder.js.map +1 -0
- package/dist/docs/embeddings/index.d.ts +102 -0
- package/dist/docs/embeddings/index.d.ts.map +1 -0
- package/dist/docs/embeddings/index.js +250 -0
- package/dist/docs/embeddings/index.js.map +1 -0
- package/dist/docs/embeddings/manager.d.ts +68 -0
- package/dist/docs/embeddings/manager.d.ts.map +1 -0
- package/dist/docs/embeddings/manager.js +97 -0
- package/dist/docs/embeddings/manager.js.map +1 -0
- package/dist/docs/fetcher.d.ts +29 -0
- package/dist/docs/fetcher.d.ts.map +1 -0
- package/dist/docs/fetcher.js +244 -0
- package/dist/docs/fetcher.js.map +1 -0
- package/dist/docs/reference.d.ts +37 -0
- package/dist/docs/reference.d.ts.map +1 -0
- package/dist/docs/reference.js +108 -0
- package/dist/docs/reference.js.map +1 -0
- package/dist/docs/search.d.ts +194 -0
- package/dist/docs/search.d.ts.map +1 -0
- package/dist/docs/search.js +733 -0
- package/dist/docs/search.js.map +1 -0
- package/dist/http-server.d.ts.map +1 -1
- package/dist/http-server.js +52 -5
- package/dist/http-server.js.map +1 -1
- package/dist/index.d.ts +8 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -1035
- package/dist/index.js.map +1 -1
- package/dist/instructions.d.ts +15 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +26 -0
- package/dist/instructions.js.map +1 -0
- package/dist/tools/defs/attributes.d.ts +6 -0
- package/dist/tools/defs/attributes.d.ts.map +1 -0
- package/dist/tools/defs/attributes.js +85 -0
- package/dist/tools/defs/attributes.js.map +1 -0
- package/dist/tools/defs/docs.d.ts +17 -0
- package/dist/tools/defs/docs.d.ts.map +1 -0
- package/dist/tools/defs/docs.js +151 -0
- package/dist/tools/defs/docs.js.map +1 -0
- package/dist/tools/defs/execute.d.ts +6 -0
- package/dist/tools/defs/execute.d.ts.map +1 -0
- package/dist/tools/defs/execute.js +21 -0
- package/dist/tools/defs/execute.js.map +1 -0
- package/dist/tools/defs/inspection.d.ts +7 -0
- package/dist/tools/defs/inspection.d.ts.map +1 -0
- package/dist/tools/defs/inspection.js +202 -0
- package/dist/tools/defs/inspection.js.map +1 -0
- package/dist/tools/defs/objects.d.ts +6 -0
- package/dist/tools/defs/objects.d.ts.map +1 -0
- package/dist/tools/defs/objects.js +111 -0
- package/dist/tools/defs/objects.js.map +1 -0
- package/dist/tools/defs/properties.d.ts +6 -0
- package/dist/tools/defs/properties.d.ts.map +1 -0
- package/dist/tools/defs/properties.js +71 -0
- package/dist/tools/defs/properties.js.map +1 -0
- package/dist/tools/defs/runtime.d.ts +6 -0
- package/dist/tools/defs/runtime.d.ts.map +1 -0
- package/dist/tools/defs/runtime.js +145 -0
- package/dist/tools/defs/runtime.js.map +1 -0
- package/dist/tools/defs/scripts.d.ts +18 -0
- package/dist/tools/defs/scripts.d.ts.map +1 -0
- package/dist/tools/defs/scripts.js +163 -0
- package/dist/tools/defs/scripts.js.map +1 -0
- package/dist/tools/defs/tags.d.ts +6 -0
- package/dist/tools/defs/tags.d.ts.map +1 -0
- package/dist/tools/defs/tags.js +74 -0
- package/dist/tools/defs/tags.js.map +1 -0
- package/dist/tools/defs/visual.d.ts +7 -0
- package/dist/tools/defs/visual.d.ts.map +1 -0
- package/dist/tools/defs/visual.js +208 -0
- package/dist/tools/defs/visual.js.map +1 -0
- package/dist/tools/index.d.ts +101 -25
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +580 -63
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/nudges.d.ts +25 -0
- package/dist/tools/nudges.d.ts.map +1 -0
- package/dist/tools/nudges.js +34 -0
- package/dist/tools/nudges.js.map +1 -0
- package/dist/tools/registry.d.ts +20 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +65 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/types.d.ts +24 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -0
- package/package.json +7 -6
- package/studio-plugin/MCPPlugin.rbxmx +3 -238
- package/studio-plugin/plugin.luau +2041 -365
package/dist/tools/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { StudioHttpClient } from './studio-client.js';
|
|
2
2
|
import * as zlib from 'zlib';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { ensureDocsCache } from '../docs/fetcher.js';
|
|
5
|
+
import { listDocs, readDocFile, searchDocs, } from '../docs/search.js';
|
|
6
|
+
import { resolveReference } from '../docs/reference.js';
|
|
3
7
|
// PNG encoding utilities
|
|
4
8
|
function createPNG(rgbaData, width, height) {
|
|
5
9
|
// PNG signature
|
|
@@ -70,6 +74,54 @@ export class RobloxStudioTools {
|
|
|
70
74
|
this.bridge = bridge;
|
|
71
75
|
this.client = new StudioHttpClient(bridge);
|
|
72
76
|
}
|
|
77
|
+
// ============================================
|
|
78
|
+
// MCP-INTERNAL SAFETY GUARDS
|
|
79
|
+
//
|
|
80
|
+
// run_live_lua relies on injected companion Scripts that live in
|
|
81
|
+
// ServerScriptService and StarterPlayer.StarterPlayerScripts. We don't
|
|
82
|
+
// want a confused AI to accidentally delete/edit/disable them via the
|
|
83
|
+
// generic destructive tools (set_property, delete_object, set_script_source,
|
|
84
|
+
// edit_script, find_and_replace_in_scripts, move_instance) — those would
|
|
85
|
+
// silently break the bridge.
|
|
86
|
+
//
|
|
87
|
+
// The plugin already does opportunistic sweeping by tag/name on activate
|
|
88
|
+
// and at the start/end of each play_solo. This guard is the second layer:
|
|
89
|
+
// refuse the operation outright with a friendly explanation, so the AI
|
|
90
|
+
// gets explicit feedback instead of mysterious "no eval reply" errors.
|
|
91
|
+
// ============================================
|
|
92
|
+
static MCP_INTERNAL_NAMES = [
|
|
93
|
+
'_MCPTestCompanion',
|
|
94
|
+
'_MCPTestCompanion_Client',
|
|
95
|
+
];
|
|
96
|
+
isMCPInternal(path) {
|
|
97
|
+
if (!path || typeof path !== 'string')
|
|
98
|
+
return false;
|
|
99
|
+
for (const name of RobloxStudioTools.MCP_INTERNAL_NAMES) {
|
|
100
|
+
// Match as a path segment (preceded/followed by '.' or end-of-string),
|
|
101
|
+
// not as a substring inside an arbitrary user-named instance.
|
|
102
|
+
if (path === name ||
|
|
103
|
+
path.endsWith('.' + name) ||
|
|
104
|
+
path.includes('.' + name + '.') ||
|
|
105
|
+
path.startsWith(name + '.')) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
mcpInternalRefusal(action, instancePath) {
|
|
112
|
+
return {
|
|
113
|
+
content: [
|
|
114
|
+
{
|
|
115
|
+
type: 'text',
|
|
116
|
+
text: JSON.stringify({
|
|
117
|
+
success: false,
|
|
118
|
+
error: 'mcp_internal',
|
|
119
|
+
message: `Refusing to ${action} "${instancePath}" — this is an MCP-internal companion Script used by run_live_lua / play_solo. The plugin manages its lifecycle automatically; if you really want it gone, run stop_play and the plugin will sweep it on the next activation. (Names reserved: _MCPTestCompanion, _MCPTestCompanion_Client.)`,
|
|
120
|
+
}, null, 2),
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
73
125
|
// File System Tools
|
|
74
126
|
async getFileTree(path = '') {
|
|
75
127
|
const response = await this.client.request('/api/file-tree', { path });
|
|
@@ -207,11 +259,57 @@ export class RobloxStudioTools {
|
|
|
207
259
|
]
|
|
208
260
|
};
|
|
209
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* grep — Claude Code-style search across the instance tree.
|
|
264
|
+
*
|
|
265
|
+
* Forwards to the Studio plugin which does a single `GetDescendants` walk
|
|
266
|
+
* scoped to `path`, filters by `type` (via `IsA`) and `glob` (Lua pattern
|
|
267
|
+
* over Name), then scans `Source` line-by-line (or full-source if
|
|
268
|
+
* `multiline`) for `pattern`. Designed to mirror the parameter surface of
|
|
269
|
+
* Claude Code's Grep tool 1:1 so the LLM can reuse muscle memory.
|
|
270
|
+
*/
|
|
271
|
+
async grep(pattern, options = {}) {
|
|
272
|
+
if (typeof pattern !== 'string' || pattern.length === 0) {
|
|
273
|
+
throw new Error('pattern is required for grep (use ".*" for name-only searches)');
|
|
274
|
+
}
|
|
275
|
+
const response = await this.client.request('/api/grep', {
|
|
276
|
+
pattern,
|
|
277
|
+
path: options.path ?? 'game',
|
|
278
|
+
glob: options.glob,
|
|
279
|
+
type: options.type ?? ['LuaSourceContainer'],
|
|
280
|
+
caseInsensitive: options.caseInsensitive ?? false,
|
|
281
|
+
after: options.after ?? 0,
|
|
282
|
+
before: options.before ?? 0,
|
|
283
|
+
context: options.context ?? 0,
|
|
284
|
+
outputMode: options.outputMode ?? 'files_with_matches',
|
|
285
|
+
headLimit: options.headLimit ?? 0,
|
|
286
|
+
multiline: options.multiline ?? false,
|
|
287
|
+
});
|
|
288
|
+
return {
|
|
289
|
+
content: [
|
|
290
|
+
{
|
|
291
|
+
type: 'text',
|
|
292
|
+
text: JSON.stringify(response, null, 2),
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
};
|
|
296
|
+
}
|
|
210
297
|
// Property Modification Tools
|
|
211
298
|
async setProperty(instancePath, propertyName, propertyValue) {
|
|
212
299
|
if (!instancePath || !propertyName) {
|
|
213
300
|
throw new Error('Instance path and property name are required for set_property');
|
|
214
301
|
}
|
|
302
|
+
// Refuse to mutate MCP-internal companions — only the destructive
|
|
303
|
+
// properties; reading/altering benign metadata (e.g. Source via the
|
|
304
|
+
// dedicated API) is already gated below.
|
|
305
|
+
if (this.isMCPInternal(instancePath)) {
|
|
306
|
+
const dangerous = new Set([
|
|
307
|
+
'Disabled', 'Source', 'Parent', 'Name', 'Archivable', 'RunContext', 'Enabled',
|
|
308
|
+
]);
|
|
309
|
+
if (dangerous.has(propertyName)) {
|
|
310
|
+
return this.mcpInternalRefusal(`set ${propertyName} on`, instancePath);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
215
313
|
const response = await this.client.request('/api/set-property', {
|
|
216
314
|
instancePath,
|
|
217
315
|
propertyName,
|
|
@@ -331,6 +429,9 @@ export class RobloxStudioTools {
|
|
|
331
429
|
if (!instancePath) {
|
|
332
430
|
throw new Error('Instance path is required for delete_object');
|
|
333
431
|
}
|
|
432
|
+
if (this.isMCPInternal(instancePath)) {
|
|
433
|
+
return this.mcpInternalRefusal('delete', instancePath);
|
|
434
|
+
}
|
|
334
435
|
const response = await this.client.request('/api/delete-object', { instancePath });
|
|
335
436
|
return {
|
|
336
437
|
content: [
|
|
@@ -416,7 +517,14 @@ export class RobloxStudioTools {
|
|
|
416
517
|
]
|
|
417
518
|
};
|
|
418
519
|
}
|
|
419
|
-
//
|
|
520
|
+
// ============================================
|
|
521
|
+
// SCRIPT MANAGEMENT TOOLS
|
|
522
|
+
//
|
|
523
|
+
// The legacy line-based partial editors (edit_script_lines /
|
|
524
|
+
// insert_script_lines / delete_script_lines) were removed in favor of
|
|
525
|
+
// the string-based editScript() below — line numbers shift after every
|
|
526
|
+
// edit, which makes line-based editing unreliable for AI workflows.
|
|
527
|
+
// ============================================
|
|
420
528
|
async getScriptSource(instancePath, startLine, endLine) {
|
|
421
529
|
if (!instancePath) {
|
|
422
530
|
throw new Error('Instance path is required for get_script_source');
|
|
@@ -435,50 +543,10 @@ export class RobloxStudioTools {
|
|
|
435
543
|
if (!instancePath || typeof source !== 'string') {
|
|
436
544
|
throw new Error('Instance path and source code string are required for set_script_source');
|
|
437
545
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
content: [
|
|
441
|
-
{
|
|
442
|
-
type: 'text',
|
|
443
|
-
text: JSON.stringify(response, null, 2)
|
|
444
|
-
}
|
|
445
|
-
]
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
// Partial Script Editing Tools
|
|
449
|
-
async editScriptLines(instancePath, startLine, endLine, newContent) {
|
|
450
|
-
if (!instancePath || !startLine || !endLine || typeof newContent !== 'string') {
|
|
451
|
-
throw new Error('Instance path, startLine, endLine, and newContent are required for edit_script_lines');
|
|
452
|
-
}
|
|
453
|
-
const response = await this.client.request('/api/edit-script-lines', { instancePath, startLine, endLine, newContent });
|
|
454
|
-
return {
|
|
455
|
-
content: [
|
|
456
|
-
{
|
|
457
|
-
type: 'text',
|
|
458
|
-
text: JSON.stringify(response, null, 2)
|
|
459
|
-
}
|
|
460
|
-
]
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
async insertScriptLines(instancePath, afterLine, newContent) {
|
|
464
|
-
if (!instancePath || typeof newContent !== 'string') {
|
|
465
|
-
throw new Error('Instance path and newContent are required for insert_script_lines');
|
|
466
|
-
}
|
|
467
|
-
const response = await this.client.request('/api/insert-script-lines', { instancePath, afterLine: afterLine || 0, newContent });
|
|
468
|
-
return {
|
|
469
|
-
content: [
|
|
470
|
-
{
|
|
471
|
-
type: 'text',
|
|
472
|
-
text: JSON.stringify(response, null, 2)
|
|
473
|
-
}
|
|
474
|
-
]
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
async deleteScriptLines(instancePath, startLine, endLine) {
|
|
478
|
-
if (!instancePath || !startLine || !endLine) {
|
|
479
|
-
throw new Error('Instance path, startLine, and endLine are required for delete_script_lines');
|
|
546
|
+
if (this.isMCPInternal(instancePath)) {
|
|
547
|
+
return this.mcpInternalRefusal('rewrite source of', instancePath);
|
|
480
548
|
}
|
|
481
|
-
const response = await this.client.request('/api/
|
|
549
|
+
const response = await this.client.request('/api/set-script-source', { instancePath, source });
|
|
482
550
|
return {
|
|
483
551
|
content: [
|
|
484
552
|
{
|
|
@@ -488,12 +556,9 @@ export class RobloxStudioTools {
|
|
|
488
556
|
]
|
|
489
557
|
};
|
|
490
558
|
}
|
|
491
|
-
// ============================================
|
|
492
|
-
// CLAUDE CODE-STYLE SCRIPT EDITING TOOLS
|
|
493
|
-
// ============================================
|
|
494
559
|
/**
|
|
495
|
-
* edit_script - String-based script editing like Claude Code's Edit tool
|
|
496
|
-
* Find exact text and replace it - no line numbers needed
|
|
560
|
+
* edit_script - String-based script editing like Claude Code's Edit tool.
|
|
561
|
+
* Find exact text and replace it - no line numbers needed.
|
|
497
562
|
*/
|
|
498
563
|
async editScript(instancePath, oldString, newString, replaceAll = false, validateAfter = true) {
|
|
499
564
|
if (!instancePath) {
|
|
@@ -508,6 +573,9 @@ export class RobloxStudioTools {
|
|
|
508
573
|
if (oldString === newString) {
|
|
509
574
|
throw new Error('old_string and new_string must be different');
|
|
510
575
|
}
|
|
576
|
+
if (this.isMCPInternal(instancePath)) {
|
|
577
|
+
return this.mcpInternalRefusal('edit', instancePath);
|
|
578
|
+
}
|
|
511
579
|
const response = await this.client.request('/api/edit-script', {
|
|
512
580
|
instancePath,
|
|
513
581
|
oldString,
|
|
@@ -525,7 +593,7 @@ export class RobloxStudioTools {
|
|
|
525
593
|
};
|
|
526
594
|
}
|
|
527
595
|
/**
|
|
528
|
-
* search_script - Search for patterns within a script (like grep)
|
|
596
|
+
* search_script - Search for patterns within a script (like grep).
|
|
529
597
|
*/
|
|
530
598
|
async searchScript(instancePath, pattern, useRegex = false, contextLines = 0) {
|
|
531
599
|
if (!instancePath || !pattern) {
|
|
@@ -547,7 +615,7 @@ export class RobloxStudioTools {
|
|
|
547
615
|
};
|
|
548
616
|
}
|
|
549
617
|
/**
|
|
550
|
-
* get_script_function - Extract a specific function from a script by name
|
|
618
|
+
* get_script_function - Extract a specific function from a script by name.
|
|
551
619
|
*/
|
|
552
620
|
async getScriptFunction(instancePath, functionName) {
|
|
553
621
|
if (!instancePath || !functionName) {
|
|
@@ -567,7 +635,7 @@ export class RobloxStudioTools {
|
|
|
567
635
|
};
|
|
568
636
|
}
|
|
569
637
|
/**
|
|
570
|
-
* find_and_replace_in_scripts -
|
|
638
|
+
* find_and_replace_in_scripts - Batch find-and-replace across multiple scripts.
|
|
571
639
|
*/
|
|
572
640
|
async findAndReplaceInScripts(paths, oldString, newString, validateAfter = true) {
|
|
573
641
|
if (!paths || paths.length === 0) {
|
|
@@ -576,12 +644,43 @@ export class RobloxStudioTools {
|
|
|
576
644
|
if (typeof oldString !== 'string' || typeof newString !== 'string') {
|
|
577
645
|
throw new Error('old_string and new_string are required');
|
|
578
646
|
}
|
|
647
|
+
// Filter out MCP-internal paths and report which ones we skipped, so
|
|
648
|
+
// the AI doesn't silently include companions in a batch rename.
|
|
649
|
+
const blocked = [];
|
|
650
|
+
const allowed = [];
|
|
651
|
+
for (const p of paths) {
|
|
652
|
+
if (this.isMCPInternal(p)) {
|
|
653
|
+
blocked.push(p);
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
allowed.push(p);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (allowed.length === 0) {
|
|
660
|
+
return {
|
|
661
|
+
content: [
|
|
662
|
+
{
|
|
663
|
+
type: 'text',
|
|
664
|
+
text: JSON.stringify({
|
|
665
|
+
success: false,
|
|
666
|
+
error: 'mcp_internal',
|
|
667
|
+
message: `All targeted paths are MCP-internal companion Scripts. Refusing to edit. Blocked: ${blocked.join(', ')}`,
|
|
668
|
+
}, null, 2),
|
|
669
|
+
},
|
|
670
|
+
],
|
|
671
|
+
};
|
|
672
|
+
}
|
|
579
673
|
const response = await this.client.request('/api/find-and-replace-in-scripts', {
|
|
580
|
-
paths,
|
|
674
|
+
paths: allowed,
|
|
581
675
|
oldString,
|
|
582
676
|
newString,
|
|
583
677
|
validateAfter
|
|
584
678
|
});
|
|
679
|
+
if (blocked.length > 0) {
|
|
680
|
+
const responseObj = response;
|
|
681
|
+
responseObj.skippedMCPInternal = blocked;
|
|
682
|
+
responseObj.note = `${blocked.length} MCP-internal companion Script(s) were skipped: ${blocked.join(', ')}`;
|
|
683
|
+
}
|
|
585
684
|
return {
|
|
586
685
|
content: [
|
|
587
686
|
{
|
|
@@ -742,6 +841,9 @@ export class RobloxStudioTools {
|
|
|
742
841
|
if (!sourcePath || !targetParent) {
|
|
743
842
|
throw new Error('Source path and target parent are required for clone_instance');
|
|
744
843
|
}
|
|
844
|
+
if (this.isMCPInternal(sourcePath)) {
|
|
845
|
+
return this.mcpInternalRefusal('clone', sourcePath);
|
|
846
|
+
}
|
|
745
847
|
const response = await this.client.request('/api/clone-instance', {
|
|
746
848
|
sourcePath,
|
|
747
849
|
targetParent,
|
|
@@ -760,6 +862,9 @@ export class RobloxStudioTools {
|
|
|
760
862
|
if (!instancePath || !newParent) {
|
|
761
863
|
throw new Error('Instance path and new parent are required for move_instance');
|
|
762
864
|
}
|
|
865
|
+
if (this.isMCPInternal(instancePath)) {
|
|
866
|
+
return this.mcpInternalRefusal('move', instancePath);
|
|
867
|
+
}
|
|
763
868
|
const response = await this.client.request('/api/move-instance', {
|
|
764
869
|
instancePath,
|
|
765
870
|
newParent
|
|
@@ -796,26 +901,54 @@ export class RobloxStudioTools {
|
|
|
796
901
|
// ============================================
|
|
797
902
|
// UNDO/REDO TOOLS
|
|
798
903
|
// ============================================
|
|
799
|
-
async undo() {
|
|
800
|
-
|
|
904
|
+
async undo(count) {
|
|
905
|
+
// Default + clamp client-side so the plugin can stay strict. Studio's
|
|
906
|
+
// own undo stack is the real source of truth; the plugin stops early if
|
|
907
|
+
// it runs out, regardless of what we ask for.
|
|
908
|
+
const safeCount = typeof count === 'number' && Number.isFinite(count)
|
|
909
|
+
? Math.max(1, Math.min(100, Math.floor(count)))
|
|
910
|
+
: 1;
|
|
911
|
+
const response = await this.client.request('/api/undo', { count: safeCount });
|
|
801
912
|
return {
|
|
802
913
|
content: [
|
|
803
914
|
{
|
|
804
915
|
type: 'text',
|
|
805
|
-
text: JSON.stringify(response, null, 2)
|
|
806
|
-
}
|
|
807
|
-
]
|
|
916
|
+
text: JSON.stringify(response, null, 2),
|
|
917
|
+
},
|
|
918
|
+
],
|
|
808
919
|
};
|
|
809
920
|
}
|
|
810
|
-
async redo() {
|
|
811
|
-
const
|
|
921
|
+
async redo(count) {
|
|
922
|
+
const safeCount = typeof count === 'number' && Number.isFinite(count)
|
|
923
|
+
? Math.max(1, Math.min(100, Math.floor(count)))
|
|
924
|
+
: 1;
|
|
925
|
+
const response = await this.client.request('/api/redo', { count: safeCount });
|
|
812
926
|
return {
|
|
813
927
|
content: [
|
|
814
928
|
{
|
|
815
929
|
type: 'text',
|
|
816
|
-
text: JSON.stringify(response, null, 2)
|
|
817
|
-
}
|
|
818
|
-
]
|
|
930
|
+
text: JSON.stringify(response, null, 2),
|
|
931
|
+
},
|
|
932
|
+
],
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
async getHistory(limit, includeDetails) {
|
|
936
|
+
// 20 is a sweet spot: enough for "what did I just do?" recall without
|
|
937
|
+
// bloating context. Plugin caps at MAX_ACTION_HISTORY (100).
|
|
938
|
+
const safeLimit = typeof limit === 'number' && Number.isFinite(limit)
|
|
939
|
+
? Math.max(1, Math.min(100, Math.floor(limit)))
|
|
940
|
+
: 20;
|
|
941
|
+
const response = await this.client.request('/api/get-history', {
|
|
942
|
+
limit: safeLimit,
|
|
943
|
+
includeDetails: includeDetails === true,
|
|
944
|
+
});
|
|
945
|
+
return {
|
|
946
|
+
content: [
|
|
947
|
+
{
|
|
948
|
+
type: 'text',
|
|
949
|
+
text: JSON.stringify(response, null, 2),
|
|
950
|
+
},
|
|
951
|
+
],
|
|
819
952
|
};
|
|
820
953
|
}
|
|
821
954
|
// ============================================
|
|
@@ -974,6 +1107,170 @@ export class RobloxStudioTools {
|
|
|
974
1107
|
};
|
|
975
1108
|
}
|
|
976
1109
|
// ============================================
|
|
1110
|
+
// RUN_LIVE_LUA — execute code inside the running play test
|
|
1111
|
+
//
|
|
1112
|
+
// Flow:
|
|
1113
|
+
// 1. Pre-flight: bridge.getTestSessionStatus() — fail fast with a
|
|
1114
|
+
// structured `error` field (no thrown exception) for the common
|
|
1115
|
+
// cases the AI needs to handle differently (no playtest, ended,
|
|
1116
|
+
// companion not yet connected, no clients on a client-target call,
|
|
1117
|
+
// loadstring disabled).
|
|
1118
|
+
// 2. Generate a replyId, register a watchdog slot.
|
|
1119
|
+
// 3. Enqueue an "eval" command on the appropriate companion's queue.
|
|
1120
|
+
// The companion picks it up on its next poll (~400ms cadence),
|
|
1121
|
+
// runs it under loadstring + xpcall + LogService capture, then
|
|
1122
|
+
// POSTs the result to /test-session/eval-result.
|
|
1123
|
+
// 4. Await the slot — resolves with the companion's reply, or with a
|
|
1124
|
+
// synthetic timeout / companion_error if the watchdog fires.
|
|
1125
|
+
//
|
|
1126
|
+
// This method NEVER throws — all failure paths come back as
|
|
1127
|
+
// `{ success: false, error: <enum>, message: ... }` so the AI can branch
|
|
1128
|
+
// on the error type without try/catch ceremony.
|
|
1129
|
+
// ============================================
|
|
1130
|
+
async runLiveLua(code, target = 'server', playerName, timeoutMs = 5000, captureLogs = true) {
|
|
1131
|
+
// Helper to wrap structured failures consistently.
|
|
1132
|
+
const fail = (error, message, extra = {}) => ({
|
|
1133
|
+
content: [
|
|
1134
|
+
{
|
|
1135
|
+
type: 'text',
|
|
1136
|
+
text: JSON.stringify({ success: false, error, message, target, ...extra }, null, 2),
|
|
1137
|
+
},
|
|
1138
|
+
],
|
|
1139
|
+
});
|
|
1140
|
+
// ---- Argument validation (still no throws — friendly errors) -------
|
|
1141
|
+
if (typeof code !== 'string' || code.length === 0) {
|
|
1142
|
+
return fail('invalid_args', 'code is required and must be a non-empty string.');
|
|
1143
|
+
}
|
|
1144
|
+
if (target !== 'server' && target !== 'client') {
|
|
1145
|
+
return fail('invalid_args', `target must be "server" or "client", got "${target}".`);
|
|
1146
|
+
}
|
|
1147
|
+
const tMs = Math.max(1000, Math.min(30000, Math.floor(Number(timeoutMs) || 5000)));
|
|
1148
|
+
// ---- Pre-flight on the bridge state --------------------------------
|
|
1149
|
+
const status = this.bridge.getTestSessionStatus();
|
|
1150
|
+
if (!status) {
|
|
1151
|
+
return fail('no_playtest', 'No play test is running. Call play_solo first to start one, then retry run_live_lua.');
|
|
1152
|
+
}
|
|
1153
|
+
if (status.status === 'ended') {
|
|
1154
|
+
return fail('playtest_ended', `The play test has already ended. Start a new one with play_solo before running live code.`);
|
|
1155
|
+
}
|
|
1156
|
+
// ---- Resolve target slot -------------------------------------------
|
|
1157
|
+
// Architecture note: HttpService is server-only in Roblox, so the client
|
|
1158
|
+
// companion CANNOT POST eval-results directly. Instead, target='client'
|
|
1159
|
+
// commands ride the SERVER queue with a `forwardTo` metadata field; the
|
|
1160
|
+
// server companion picks them up, dispatches via clientRelay:InvokeClient,
|
|
1161
|
+
// and POSTs the result on the client's behalf. This means the bridge's
|
|
1162
|
+
// reply slot is always 'server' regardless of logical target.
|
|
1163
|
+
let resolvedClientName = null;
|
|
1164
|
+
let resolvedClientUserId = null;
|
|
1165
|
+
const evalArgs = {
|
|
1166
|
+
// replyId added below
|
|
1167
|
+
code,
|
|
1168
|
+
timeoutMs: tMs,
|
|
1169
|
+
captureLogs: !!captureLogs,
|
|
1170
|
+
};
|
|
1171
|
+
if (target === 'server') {
|
|
1172
|
+
if (!status.serverReady) {
|
|
1173
|
+
return fail('companion_not_ready', 'Server companion has not connected yet. The play test was started but the in-test Script has not made its first poll. Wait ~1s and retry.');
|
|
1174
|
+
}
|
|
1175
|
+
if (!status.serverLoadstringReady) {
|
|
1176
|
+
return fail('loadstring_disabled', 'ServerScriptService.LoadStringEnabled is false in this place, so the server companion cannot loadstring user code. Enable LoadStringEnabled in Studio (Game Settings → Security → Allow HTTP Requests / LoadString) BEFORE starting the test, then re-run play_solo.');
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
else {
|
|
1180
|
+
// target === 'client'
|
|
1181
|
+
const clients = status.clients;
|
|
1182
|
+
if (clients.length === 0) {
|
|
1183
|
+
return fail('no_clients_connected', 'No client companion has registered yet. Either no Player has joined the test, the LocalScript hasn\'t fired its hello RemoteEvent yet, or the server companion isn\'t set up. Wait briefly, or fall back to target="server".');
|
|
1184
|
+
}
|
|
1185
|
+
let chosen = clients[0];
|
|
1186
|
+
if (playerName) {
|
|
1187
|
+
const match = clients.find((c) => c.name === playerName || c.name.toLowerCase() === playerName.toLowerCase());
|
|
1188
|
+
if (!match) {
|
|
1189
|
+
return fail('no_such_player', `No connected client named "${playerName}". Connected players: ${clients
|
|
1190
|
+
.map((c) => `${c.name} (userId=${c.userId})`)
|
|
1191
|
+
.join(', ')}.`, { connectedClients: clients });
|
|
1192
|
+
}
|
|
1193
|
+
chosen = match;
|
|
1194
|
+
}
|
|
1195
|
+
else if (clients.length > 1) {
|
|
1196
|
+
// Multi-client play test, ambiguous — bail loudly.
|
|
1197
|
+
return fail('multiple_clients', `Multiple clients are connected; pass playerName to disambiguate. Connected: ${clients
|
|
1198
|
+
.map((c) => `${c.name} (userId=${c.userId})`)
|
|
1199
|
+
.join(', ')}.`, { connectedClients: clients });
|
|
1200
|
+
}
|
|
1201
|
+
if (!chosen.ready) {
|
|
1202
|
+
return fail('companion_not_ready', `Client companion for ${chosen.name} (userId=${chosen.userId}) has not finished its hello yet. Wait briefly and retry.`);
|
|
1203
|
+
}
|
|
1204
|
+
// Loadstring is core (always enabled in client LocalScripts), but we
|
|
1205
|
+
// surface the flag for completeness if the companion ever reports
|
|
1206
|
+
// false.
|
|
1207
|
+
if (!chosen.loadstringReady) {
|
|
1208
|
+
return fail('loadstring_disabled', `Client companion for ${chosen.name} reported loadstring unavailable. (This is unusual on the client; check that the LocalScript was injected correctly.)`);
|
|
1209
|
+
}
|
|
1210
|
+
resolvedClientName = chosen.name;
|
|
1211
|
+
resolvedClientUserId = chosen.userId;
|
|
1212
|
+
// forwardTo tells the server companion to InvokeClient this player
|
|
1213
|
+
// instead of running the code locally on the server.
|
|
1214
|
+
evalArgs.forwardTo = { userId: chosen.userId, playerName: chosen.name };
|
|
1215
|
+
}
|
|
1216
|
+
// ---- Register slot, then enqueue the command ----------------------
|
|
1217
|
+
// Use the bridge's enqueue first because if it returns false we want
|
|
1218
|
+
// to skip allocating a watcher promise. But we need the replyId baked
|
|
1219
|
+
// into the enqueued args, so generate it up front.
|
|
1220
|
+
const replyId = randomUUID();
|
|
1221
|
+
evalArgs.replyId = replyId;
|
|
1222
|
+
// Register the watchdog slot BEFORE enqueueing. We always use 'server'
|
|
1223
|
+
// as the slot target because the server companion is what actually
|
|
1224
|
+
// POSTs /test-session/eval-result back to us (even for client-target
|
|
1225
|
+
// evals — see forwardTo dispatch in the server companion template).
|
|
1226
|
+
const replyPromise = this.bridge.registerEvalReply(replyId, 'server', tMs);
|
|
1227
|
+
const queued = this.bridge.enqueueTestCommand({
|
|
1228
|
+
cmd: 'eval',
|
|
1229
|
+
args: evalArgs,
|
|
1230
|
+
}, 'server');
|
|
1231
|
+
if (!queued) {
|
|
1232
|
+
// Bridge couldn't accept (session ended between status check and
|
|
1233
|
+
// enqueue, or invalid target). The watchdog will eventually trigger
|
|
1234
|
+
// a companion_error reply, but we'd rather not wait a full grace
|
|
1235
|
+
// window — return immediately.
|
|
1236
|
+
return fail('companion_error', 'Failed to enqueue eval command on the bridge (session may have just ended). Re-check play_solo state.');
|
|
1237
|
+
}
|
|
1238
|
+
// Await the reply (will always resolve — watchdog guarantees it).
|
|
1239
|
+
const startedAt = Date.now();
|
|
1240
|
+
const reply = await replyPromise;
|
|
1241
|
+
const totalDurationMs = Date.now() - startedAt;
|
|
1242
|
+
// ---- Map the reply into the canonical Result shape ----------------
|
|
1243
|
+
const baseResult = {
|
|
1244
|
+
success: !!reply.ok,
|
|
1245
|
+
target,
|
|
1246
|
+
timeoutMs: tMs,
|
|
1247
|
+
durationMs: typeof reply.durationMs === 'number' ? reply.durationMs : totalDurationMs,
|
|
1248
|
+
};
|
|
1249
|
+
if (resolvedClientName)
|
|
1250
|
+
baseResult.player = { name: resolvedClientName, userId: resolvedClientUserId };
|
|
1251
|
+
if (reply.ok) {
|
|
1252
|
+
baseResult.values = reply.values ?? [];
|
|
1253
|
+
if (reply.logs && reply.logs.length > 0)
|
|
1254
|
+
baseResult.logs = reply.logs;
|
|
1255
|
+
}
|
|
1256
|
+
else {
|
|
1257
|
+
baseResult.error = reply.errorType ?? 'companion_error';
|
|
1258
|
+
baseResult.message = reply.error ?? 'Unknown eval failure.';
|
|
1259
|
+
if (reply.traceback)
|
|
1260
|
+
baseResult.traceback = reply.traceback;
|
|
1261
|
+
if (reply.logs && reply.logs.length > 0)
|
|
1262
|
+
baseResult.logs = reply.logs;
|
|
1263
|
+
}
|
|
1264
|
+
return {
|
|
1265
|
+
content: [
|
|
1266
|
+
{
|
|
1267
|
+
type: 'text',
|
|
1268
|
+
text: JSON.stringify(baseResult, null, 2),
|
|
1269
|
+
},
|
|
1270
|
+
],
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
// ============================================
|
|
977
1274
|
// SCREENSHOT TOOL
|
|
978
1275
|
// ============================================
|
|
979
1276
|
async captureScreenshot(maxWidth, maxHeight) {
|
|
@@ -1122,6 +1419,80 @@ export class RobloxStudioTools {
|
|
|
1122
1419
|
],
|
|
1123
1420
|
};
|
|
1124
1421
|
}
|
|
1422
|
+
/**
|
|
1423
|
+
* render_gui - Render a 2D GUI (ScreenGui / GuiObject) to a PNG image.
|
|
1424
|
+
*
|
|
1425
|
+
* ViewportFrame is 3D-only, so GUIs are captured plugin-side by cloning the
|
|
1426
|
+
* target's real ScreenGui into CoreGui (faithful placement), taking a
|
|
1427
|
+
* full-screen CaptureService screenshot, then cropping. region "element"
|
|
1428
|
+
* (default) crops tight to the element's rect; "screen" returns the whole
|
|
1429
|
+
* viewport so on-screen placement can be verified. Here we just convert the
|
|
1430
|
+
* returned RGBA to PNG, mirroring renderObjectView.
|
|
1431
|
+
*/
|
|
1432
|
+
async renderGui(instancePath, options) {
|
|
1433
|
+
if (!instancePath) {
|
|
1434
|
+
throw new Error('Instance path is required for render_gui');
|
|
1435
|
+
}
|
|
1436
|
+
const response = await this.client.request('/api/render-gui', {
|
|
1437
|
+
instancePath,
|
|
1438
|
+
region: options?.region || 'element',
|
|
1439
|
+
maxWidth: options?.maxWidth,
|
|
1440
|
+
maxHeight: options?.maxHeight,
|
|
1441
|
+
});
|
|
1442
|
+
const responseData = response;
|
|
1443
|
+
if (responseData.success && responseData.base64) {
|
|
1444
|
+
try {
|
|
1445
|
+
const rgbaBuffer = Buffer.from(responseData.base64, 'base64');
|
|
1446
|
+
const width = responseData.width;
|
|
1447
|
+
const height = responseData.height;
|
|
1448
|
+
const expectedSize = width * height * 4;
|
|
1449
|
+
if (rgbaBuffer.length !== expectedSize) {
|
|
1450
|
+
throw new Error(`Buffer size mismatch: got ${rgbaBuffer.length}, expected ${expectedSize}`);
|
|
1451
|
+
}
|
|
1452
|
+
const pngBuffer = createPNG(rgbaBuffer, width, height);
|
|
1453
|
+
const pngBase64 = pngBuffer.toString('base64');
|
|
1454
|
+
return {
|
|
1455
|
+
content: [
|
|
1456
|
+
{
|
|
1457
|
+
type: 'text',
|
|
1458
|
+
text: JSON.stringify({
|
|
1459
|
+
success: true,
|
|
1460
|
+
message: responseData.message,
|
|
1461
|
+
guiInfo: responseData.guiInfo,
|
|
1462
|
+
format: 'PNG',
|
|
1463
|
+
}, null, 2),
|
|
1464
|
+
},
|
|
1465
|
+
{
|
|
1466
|
+
type: 'image',
|
|
1467
|
+
data: pngBase64,
|
|
1468
|
+
mimeType: 'image/png',
|
|
1469
|
+
},
|
|
1470
|
+
],
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
catch (err) {
|
|
1474
|
+
return {
|
|
1475
|
+
content: [
|
|
1476
|
+
{
|
|
1477
|
+
type: 'text',
|
|
1478
|
+
text: JSON.stringify({
|
|
1479
|
+
success: false,
|
|
1480
|
+
error: `PNG conversion failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1481
|
+
}, null, 2),
|
|
1482
|
+
},
|
|
1483
|
+
],
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
return {
|
|
1488
|
+
content: [
|
|
1489
|
+
{
|
|
1490
|
+
type: 'text',
|
|
1491
|
+
text: JSON.stringify(response, null, 2),
|
|
1492
|
+
},
|
|
1493
|
+
],
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1125
1496
|
// ============================================
|
|
1126
1497
|
// CAMERA CONTROL SYSTEM
|
|
1127
1498
|
// ============================================
|
|
@@ -1165,5 +1536,151 @@ export class RobloxStudioTools {
|
|
|
1165
1536
|
]
|
|
1166
1537
|
};
|
|
1167
1538
|
}
|
|
1539
|
+
// ============================================
|
|
1540
|
+
// ROBLOX CREATOR-DOCS TOOLS
|
|
1541
|
+
//
|
|
1542
|
+
// These tools mirror github.com/Roblox/creator-docs to a local cache
|
|
1543
|
+
// and let the model search/read it like a local repo. They do NOT go
|
|
1544
|
+
// through the Studio HTTP bridge — they're pure server-side.
|
|
1545
|
+
//
|
|
1546
|
+
// The cache is downloaded lazily on first use (~5s, ~30MB) and
|
|
1547
|
+
// refreshed at most once every 24h with a SHA short-circuit. See
|
|
1548
|
+
// src/docs/fetcher.ts for the strategy.
|
|
1549
|
+
// ============================================
|
|
1550
|
+
async searchRobloxDocs(query, options = {}) {
|
|
1551
|
+
if (!query || typeof query !== 'string') {
|
|
1552
|
+
throw new Error('query is required for search_roblox_docs');
|
|
1553
|
+
}
|
|
1554
|
+
const ensured = await ensureDocsCache();
|
|
1555
|
+
// Plumb the docs SHA through so hybrid mode can locate / build the
|
|
1556
|
+
// matching semantic index. Without this, hybrid mode degrades to
|
|
1557
|
+
// pure keyword (semanticUsed=false in the response).
|
|
1558
|
+
const summary = await searchDocs(ensured.cacheDir, query, options, {
|
|
1559
|
+
docsSha: ensured.meta.sha,
|
|
1560
|
+
});
|
|
1561
|
+
return {
|
|
1562
|
+
content: [
|
|
1563
|
+
{
|
|
1564
|
+
type: 'text',
|
|
1565
|
+
text: JSON.stringify({
|
|
1566
|
+
cache: {
|
|
1567
|
+
action: ensured.action,
|
|
1568
|
+
sha: ensured.meta.sha,
|
|
1569
|
+
downloadedAt: ensured.meta.downloadedAt,
|
|
1570
|
+
durationMs: ensured.durationMs,
|
|
1571
|
+
},
|
|
1572
|
+
query,
|
|
1573
|
+
options,
|
|
1574
|
+
...summary,
|
|
1575
|
+
}, null, 2),
|
|
1576
|
+
},
|
|
1577
|
+
],
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
async getRobloxDoc(relPath) {
|
|
1581
|
+
if (!relPath || typeof relPath !== 'string') {
|
|
1582
|
+
throw new Error('path is required for get_roblox_doc');
|
|
1583
|
+
}
|
|
1584
|
+
const ensured = await ensureDocsCache();
|
|
1585
|
+
const doc = await readDocFile(ensured.cacheDir, relPath);
|
|
1586
|
+
if (!doc) {
|
|
1587
|
+
return {
|
|
1588
|
+
content: [
|
|
1589
|
+
{
|
|
1590
|
+
type: 'text',
|
|
1591
|
+
text: JSON.stringify({
|
|
1592
|
+
error: `Doc not found: "${relPath}". Use list_roblox_docs to discover paths, or search_roblox_docs to find a hit.`,
|
|
1593
|
+
cache: {
|
|
1594
|
+
sha: ensured.meta.sha,
|
|
1595
|
+
fileCount: ensured.meta.fileCount,
|
|
1596
|
+
cacheDir: ensured.cacheDir,
|
|
1597
|
+
},
|
|
1598
|
+
}, null, 2),
|
|
1599
|
+
},
|
|
1600
|
+
],
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
return {
|
|
1604
|
+
content: [
|
|
1605
|
+
{
|
|
1606
|
+
type: 'text',
|
|
1607
|
+
text: JSON.stringify({
|
|
1608
|
+
path: doc.path,
|
|
1609
|
+
bytes: doc.bytes,
|
|
1610
|
+
cache: {
|
|
1611
|
+
action: ensured.action,
|
|
1612
|
+
sha: ensured.meta.sha,
|
|
1613
|
+
},
|
|
1614
|
+
content: doc.content,
|
|
1615
|
+
}, null, 2),
|
|
1616
|
+
},
|
|
1617
|
+
],
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
async listRobloxDocs(relPath = '', options = {}) {
|
|
1621
|
+
const ensured = await ensureDocsCache();
|
|
1622
|
+
const listing = await listDocs(ensured.cacheDir, relPath, options);
|
|
1623
|
+
if (!listing) {
|
|
1624
|
+
return {
|
|
1625
|
+
content: [
|
|
1626
|
+
{
|
|
1627
|
+
type: 'text',
|
|
1628
|
+
text: JSON.stringify({
|
|
1629
|
+
error: `Path not found: "${relPath}".`,
|
|
1630
|
+
cache: { sha: ensured.meta.sha, fileCount: ensured.meta.fileCount },
|
|
1631
|
+
}, null, 2),
|
|
1632
|
+
},
|
|
1633
|
+
],
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
return {
|
|
1637
|
+
content: [
|
|
1638
|
+
{
|
|
1639
|
+
type: 'text',
|
|
1640
|
+
text: JSON.stringify({
|
|
1641
|
+
cache: {
|
|
1642
|
+
action: ensured.action,
|
|
1643
|
+
sha: ensured.meta.sha,
|
|
1644
|
+
},
|
|
1645
|
+
...listing,
|
|
1646
|
+
}, null, 2),
|
|
1647
|
+
},
|
|
1648
|
+
],
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
async getRobloxApiReference(name, category) {
|
|
1652
|
+
if (!name || typeof name !== 'string') {
|
|
1653
|
+
throw new Error('name is required for get_roblox_api_reference');
|
|
1654
|
+
}
|
|
1655
|
+
const ensured = await ensureDocsCache();
|
|
1656
|
+
const result = await resolveReference(ensured.cacheDir, name, category);
|
|
1657
|
+
if (!result) {
|
|
1658
|
+
return {
|
|
1659
|
+
content: [
|
|
1660
|
+
{
|
|
1661
|
+
type: 'text',
|
|
1662
|
+
text: JSON.stringify({
|
|
1663
|
+
error: `No API reference found for "${name}"${category ? ` in category "${category}"` : ''}. Try search_roblox_docs to find similar names.`,
|
|
1664
|
+
cache: { sha: ensured.meta.sha, fileCount: ensured.meta.fileCount },
|
|
1665
|
+
}, null, 2),
|
|
1666
|
+
},
|
|
1667
|
+
],
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
return {
|
|
1671
|
+
content: [
|
|
1672
|
+
{
|
|
1673
|
+
type: 'text',
|
|
1674
|
+
text: JSON.stringify({
|
|
1675
|
+
cache: {
|
|
1676
|
+
action: ensured.action,
|
|
1677
|
+
sha: ensured.meta.sha,
|
|
1678
|
+
},
|
|
1679
|
+
...result,
|
|
1680
|
+
}, null, 2),
|
|
1681
|
+
},
|
|
1682
|
+
],
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1168
1685
|
}
|
|
1169
1686
|
//# sourceMappingURL=index.js.map
|