unreal-engine-mcp-server 0.2.1

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.
Files changed (155) hide show
  1. package/.dockerignore +57 -0
  2. package/.env.production +25 -0
  3. package/.eslintrc.json +54 -0
  4. package/.github/workflows/publish-mcp.yml +75 -0
  5. package/Dockerfile +54 -0
  6. package/LICENSE +21 -0
  7. package/Public/icon.png +0 -0
  8. package/README.md +209 -0
  9. package/claude_desktop_config_example.json +13 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.js +7 -0
  12. package/dist/index.d.ts +31 -0
  13. package/dist/index.js +484 -0
  14. package/dist/prompts/index.d.ts +14 -0
  15. package/dist/prompts/index.js +38 -0
  16. package/dist/python-utils.d.ts +29 -0
  17. package/dist/python-utils.js +54 -0
  18. package/dist/resources/actors.d.ts +13 -0
  19. package/dist/resources/actors.js +83 -0
  20. package/dist/resources/assets.d.ts +23 -0
  21. package/dist/resources/assets.js +245 -0
  22. package/dist/resources/levels.d.ts +17 -0
  23. package/dist/resources/levels.js +94 -0
  24. package/dist/tools/actors.d.ts +51 -0
  25. package/dist/tools/actors.js +459 -0
  26. package/dist/tools/animation.d.ts +196 -0
  27. package/dist/tools/animation.js +579 -0
  28. package/dist/tools/assets.d.ts +21 -0
  29. package/dist/tools/assets.js +304 -0
  30. package/dist/tools/audio.d.ts +170 -0
  31. package/dist/tools/audio.js +416 -0
  32. package/dist/tools/blueprint.d.ts +144 -0
  33. package/dist/tools/blueprint.js +652 -0
  34. package/dist/tools/build_environment_advanced.d.ts +66 -0
  35. package/dist/tools/build_environment_advanced.js +484 -0
  36. package/dist/tools/consolidated-tool-definitions.d.ts +2598 -0
  37. package/dist/tools/consolidated-tool-definitions.js +607 -0
  38. package/dist/tools/consolidated-tool-handlers.d.ts +2 -0
  39. package/dist/tools/consolidated-tool-handlers.js +1050 -0
  40. package/dist/tools/debug.d.ts +185 -0
  41. package/dist/tools/debug.js +265 -0
  42. package/dist/tools/editor.d.ts +88 -0
  43. package/dist/tools/editor.js +365 -0
  44. package/dist/tools/engine.d.ts +30 -0
  45. package/dist/tools/engine.js +36 -0
  46. package/dist/tools/foliage.d.ts +155 -0
  47. package/dist/tools/foliage.js +525 -0
  48. package/dist/tools/introspection.d.ts +98 -0
  49. package/dist/tools/introspection.js +683 -0
  50. package/dist/tools/landscape.d.ts +158 -0
  51. package/dist/tools/landscape.js +375 -0
  52. package/dist/tools/level.d.ts +110 -0
  53. package/dist/tools/level.js +362 -0
  54. package/dist/tools/lighting.d.ts +159 -0
  55. package/dist/tools/lighting.js +1179 -0
  56. package/dist/tools/materials.d.ts +34 -0
  57. package/dist/tools/materials.js +146 -0
  58. package/dist/tools/niagara.d.ts +145 -0
  59. package/dist/tools/niagara.js +289 -0
  60. package/dist/tools/performance.d.ts +163 -0
  61. package/dist/tools/performance.js +412 -0
  62. package/dist/tools/physics.d.ts +189 -0
  63. package/dist/tools/physics.js +784 -0
  64. package/dist/tools/rc.d.ts +110 -0
  65. package/dist/tools/rc.js +363 -0
  66. package/dist/tools/sequence.d.ts +112 -0
  67. package/dist/tools/sequence.js +675 -0
  68. package/dist/tools/tool-definitions.d.ts +4919 -0
  69. package/dist/tools/tool-definitions.js +891 -0
  70. package/dist/tools/tool-handlers.d.ts +47 -0
  71. package/dist/tools/tool-handlers.js +830 -0
  72. package/dist/tools/ui.d.ts +171 -0
  73. package/dist/tools/ui.js +337 -0
  74. package/dist/tools/visual.d.ts +29 -0
  75. package/dist/tools/visual.js +67 -0
  76. package/dist/types/env.d.ts +10 -0
  77. package/dist/types/env.js +18 -0
  78. package/dist/types/index.d.ts +323 -0
  79. package/dist/types/index.js +28 -0
  80. package/dist/types/tool-types.d.ts +274 -0
  81. package/dist/types/tool-types.js +13 -0
  82. package/dist/unreal-bridge.d.ts +126 -0
  83. package/dist/unreal-bridge.js +992 -0
  84. package/dist/utils/cache-manager.d.ts +64 -0
  85. package/dist/utils/cache-manager.js +176 -0
  86. package/dist/utils/error-handler.d.ts +66 -0
  87. package/dist/utils/error-handler.js +243 -0
  88. package/dist/utils/errors.d.ts +133 -0
  89. package/dist/utils/errors.js +256 -0
  90. package/dist/utils/http.d.ts +26 -0
  91. package/dist/utils/http.js +135 -0
  92. package/dist/utils/logger.d.ts +12 -0
  93. package/dist/utils/logger.js +32 -0
  94. package/dist/utils/normalize.d.ts +17 -0
  95. package/dist/utils/normalize.js +49 -0
  96. package/dist/utils/response-validator.d.ts +34 -0
  97. package/dist/utils/response-validator.js +121 -0
  98. package/dist/utils/safe-json.d.ts +4 -0
  99. package/dist/utils/safe-json.js +97 -0
  100. package/dist/utils/stdio-redirect.d.ts +2 -0
  101. package/dist/utils/stdio-redirect.js +20 -0
  102. package/dist/utils/validation.d.ts +50 -0
  103. package/dist/utils/validation.js +173 -0
  104. package/mcp-config-example.json +14 -0
  105. package/package.json +63 -0
  106. package/server.json +60 -0
  107. package/src/cli.ts +7 -0
  108. package/src/index.ts +543 -0
  109. package/src/prompts/index.ts +51 -0
  110. package/src/python/editor_compat.py +181 -0
  111. package/src/python-utils.ts +57 -0
  112. package/src/resources/actors.ts +92 -0
  113. package/src/resources/assets.ts +251 -0
  114. package/src/resources/levels.ts +83 -0
  115. package/src/tools/actors.ts +480 -0
  116. package/src/tools/animation.ts +713 -0
  117. package/src/tools/assets.ts +305 -0
  118. package/src/tools/audio.ts +548 -0
  119. package/src/tools/blueprint.ts +736 -0
  120. package/src/tools/build_environment_advanced.ts +526 -0
  121. package/src/tools/consolidated-tool-definitions.ts +619 -0
  122. package/src/tools/consolidated-tool-handlers.ts +1093 -0
  123. package/src/tools/debug.ts +368 -0
  124. package/src/tools/editor.ts +360 -0
  125. package/src/tools/engine.ts +32 -0
  126. package/src/tools/foliage.ts +652 -0
  127. package/src/tools/introspection.ts +778 -0
  128. package/src/tools/landscape.ts +523 -0
  129. package/src/tools/level.ts +410 -0
  130. package/src/tools/lighting.ts +1316 -0
  131. package/src/tools/materials.ts +148 -0
  132. package/src/tools/niagara.ts +312 -0
  133. package/src/tools/performance.ts +549 -0
  134. package/src/tools/physics.ts +924 -0
  135. package/src/tools/rc.ts +437 -0
  136. package/src/tools/sequence.ts +791 -0
  137. package/src/tools/tool-definitions.ts +907 -0
  138. package/src/tools/tool-handlers.ts +941 -0
  139. package/src/tools/ui.ts +499 -0
  140. package/src/tools/visual.ts +60 -0
  141. package/src/types/env.ts +27 -0
  142. package/src/types/index.ts +414 -0
  143. package/src/types/tool-types.ts +343 -0
  144. package/src/unreal-bridge.ts +1118 -0
  145. package/src/utils/cache-manager.ts +213 -0
  146. package/src/utils/error-handler.ts +320 -0
  147. package/src/utils/errors.ts +312 -0
  148. package/src/utils/http.ts +184 -0
  149. package/src/utils/logger.ts +30 -0
  150. package/src/utils/normalize.ts +54 -0
  151. package/src/utils/response-validator.ts +145 -0
  152. package/src/utils/safe-json.ts +112 -0
  153. package/src/utils/stdio-redirect.ts +18 -0
  154. package/src/utils/validation.ts +212 -0
  155. package/tsconfig.json +33 -0
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Cache Manager for API responses
3
+ * Implements LRU cache with TTL support for optimizing repeated API calls
4
+ */
5
+
6
+ interface CacheEntry<T> {
7
+ data: T;
8
+ timestamp: number;
9
+ hits: number;
10
+ }
11
+
12
+ interface CacheOptions {
13
+ maxSize?: number;
14
+ defaultTTL?: number; // in milliseconds
15
+ enableMetrics?: boolean;
16
+ }
17
+
18
+ interface CacheMetrics {
19
+ hits: number;
20
+ misses: number;
21
+ evictions: number;
22
+ size: number;
23
+ }
24
+
25
+ export class CacheManager<T = any> {
26
+ private cache: Map<string, CacheEntry<T>>;
27
+ private readonly maxSize: number;
28
+ private readonly defaultTTL: number;
29
+ private readonly enableMetrics: boolean;
30
+ private metrics: CacheMetrics;
31
+
32
+ constructor(options: CacheOptions = {}) {
33
+ this.cache = new Map();
34
+ this.maxSize = options.maxSize || 100;
35
+ this.defaultTTL = options.defaultTTL || 60000; // 1 minute default
36
+ this.enableMetrics = options.enableMetrics || false;
37
+ this.metrics = {
38
+ hits: 0,
39
+ misses: 0,
40
+ evictions: 0,
41
+ size: 0
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Get item from cache
47
+ */
48
+ get(key: string): T | null {
49
+ const entry = this.cache.get(key);
50
+
51
+ if (!entry) {
52
+ if (this.enableMetrics) this.metrics.misses++;
53
+ return null;
54
+ }
55
+
56
+ // Check if expired
57
+ if (Date.now() - entry.timestamp > this.defaultTTL) {
58
+ this.cache.delete(key);
59
+ if (this.enableMetrics) {
60
+ this.metrics.misses++;
61
+ this.metrics.size--;
62
+ }
63
+ return null;
64
+ }
65
+
66
+ // Update hit count and move to end (LRU)
67
+ entry.hits++;
68
+ this.cache.delete(key);
69
+ this.cache.set(key, entry);
70
+
71
+ if (this.enableMetrics) this.metrics.hits++;
72
+ return entry.data;
73
+ }
74
+
75
+ /**
76
+ * Set item in cache
77
+ */
78
+ set(key: string, data: T): void {
79
+ // Evict oldest if at max size
80
+ if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
81
+ const firstKey = this.cache.keys().next().value;
82
+ if (firstKey) {
83
+ this.cache.delete(firstKey);
84
+ if (this.enableMetrics) {
85
+ this.metrics.evictions++;
86
+ this.metrics.size--;
87
+ }
88
+ }
89
+ }
90
+
91
+ const entry: CacheEntry<T> = {
92
+ data,
93
+ timestamp: Date.now(),
94
+ hits: 0
95
+ };
96
+
97
+ this.cache.set(key, entry);
98
+ if (this.enableMetrics) this.metrics.size = this.cache.size;
99
+ }
100
+
101
+ /**
102
+ * Check if key exists and is valid
103
+ */
104
+ has(key: string): boolean {
105
+ const entry = this.cache.get(key);
106
+ if (!entry) return false;
107
+
108
+ // Check expiration
109
+ if (Date.now() - entry.timestamp > this.defaultTTL) {
110
+ this.cache.delete(key);
111
+ if (this.enableMetrics) this.metrics.size--;
112
+ return false;
113
+ }
114
+
115
+ return true;
116
+ }
117
+
118
+ /**
119
+ * Clear specific key or all cache
120
+ */
121
+ clear(key?: string): void {
122
+ if (key) {
123
+ this.cache.delete(key);
124
+ if (this.enableMetrics) this.metrics.size = this.cache.size;
125
+ } else {
126
+ this.cache.clear();
127
+ if (this.enableMetrics) {
128
+ this.metrics.size = 0;
129
+ }
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Get cache metrics
135
+ */
136
+ getMetrics(): CacheMetrics {
137
+ return { ...this.metrics };
138
+ }
139
+
140
+ /**
141
+ * Get cache hit rate
142
+ */
143
+ getHitRate(): number {
144
+ const total = this.metrics.hits + this.metrics.misses;
145
+ return total > 0 ? this.metrics.hits / total : 0;
146
+ }
147
+
148
+ /**
149
+ * Wrap async function with cache
150
+ */
151
+ async wrap<R = T>(
152
+ key: string,
153
+ fn: () => Promise<R>
154
+ ): Promise<R> {
155
+ // Check cache first
156
+ const cached = this.get(key) as R | null;
157
+ if (cached !== null) {
158
+ return cached;
159
+ }
160
+
161
+ // Execute function and cache result
162
+ const result = await fn();
163
+ this.set(key, result as any);
164
+ return result;
165
+ }
166
+
167
+ /**
168
+ * Batch get multiple keys
169
+ */
170
+ getBatch(keys: string[]): Map<string, T | null> {
171
+ const results = new Map<string, T | null>();
172
+ for (const key of keys) {
173
+ results.set(key, this.get(key));
174
+ }
175
+ return results;
176
+ }
177
+
178
+ /**
179
+ * Invalidate cache entries by pattern
180
+ */
181
+ invalidatePattern(pattern: RegExp): number {
182
+ let count = 0;
183
+ for (const key of this.cache.keys()) {
184
+ if (pattern.test(key)) {
185
+ this.cache.delete(key);
186
+ count++;
187
+ }
188
+ }
189
+ if (this.enableMetrics) {
190
+ this.metrics.size = this.cache.size;
191
+ }
192
+ return count;
193
+ }
194
+ }
195
+
196
+ // Global cache instances for different purposes
197
+ export const assetCache = new CacheManager({
198
+ maxSize: 500,
199
+ defaultTTL: 300000, // 5 minutes for assets
200
+ enableMetrics: true
201
+ });
202
+
203
+ export const engineCache = new CacheManager({
204
+ maxSize: 50,
205
+ defaultTTL: 600000, // 10 minutes for engine info
206
+ enableMetrics: true
207
+ });
208
+
209
+ export const commandCache = new CacheManager({
210
+ maxSize: 100,
211
+ defaultTTL: 30000, // 30 seconds for commands
212
+ enableMetrics: true
213
+ });
@@ -0,0 +1,320 @@
1
+ import { Logger } from './logger.js';
2
+ import { BaseToolResponse } from '../types/tool-types.js';
3
+
4
+ const log = new Logger('ErrorHandler');
5
+
6
+ /**
7
+ * Error types for categorization
8
+ */
9
+ export enum ErrorType {
10
+ VALIDATION = 'VALIDATION',
11
+ CONNECTION = 'CONNECTION',
12
+ UNREAL_ENGINE = 'UNREAL_ENGINE',
13
+ PARAMETER = 'PARAMETER',
14
+ EXECUTION = 'EXECUTION',
15
+ TIMEOUT = 'TIMEOUT',
16
+ UNKNOWN = 'UNKNOWN'
17
+ }
18
+
19
+ /**
20
+ * Custom error class for MCP tools
21
+ */
22
+ export class ToolError extends Error {
23
+ constructor(
24
+ public type: ErrorType,
25
+ public toolName: string,
26
+ message: string,
27
+ public originalError?: any
28
+ ) {
29
+ super(message);
30
+ this.name = 'ToolError';
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Consistent error handling for all tools
36
+ */
37
+ export class ErrorHandler {
38
+ /**
39
+ * Create a standardized error response
40
+ */
41
+ static createErrorResponse(
42
+ error: any,
43
+ toolName: string,
44
+ context?: any
45
+ ): BaseToolResponse {
46
+ const errorType = this.categorizeError(error);
47
+ const userMessage = this.getUserFriendlyMessage(errorType, error);
48
+ const retriable = this.isRetriable(error);
49
+ const scope = context?.scope || `tool-call/${toolName}`;
50
+
51
+ log.error(`Tool ${toolName} failed:`, {
52
+ type: errorType,
53
+ message: error.message || error,
54
+ retriable,
55
+ scope,
56
+ context
57
+ });
58
+
59
+ return {
60
+ success: false,
61
+ error: userMessage,
62
+ message: `Failed to execute ${toolName}: ${userMessage}`,
63
+ retriable: retriable as any,
64
+ scope: scope as any,
65
+ // Add debug info in development
66
+ ...(process.env.NODE_ENV === 'development' && {
67
+ _debug: {
68
+ errorType,
69
+ originalError: error.message || String(error),
70
+ stack: error.stack,
71
+ context,
72
+ retriable,
73
+ scope
74
+ }
75
+ })
76
+ } as any;
77
+ }
78
+
79
+ /**
80
+ * Create a standardized warning response
81
+ */
82
+ static createWarningResponse(
83
+ message: string,
84
+ result: any,
85
+ toolName: string
86
+ ): BaseToolResponse {
87
+ log.warn(`Tool ${toolName} warning: ${message}`);
88
+
89
+ return {
90
+ success: true,
91
+ warning: message,
92
+ message: `${toolName} completed with warnings`,
93
+ ...result
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Create a standardized success response
99
+ */
100
+ static createSuccessResponse(
101
+ message: string,
102
+ data: any = {}
103
+ ): BaseToolResponse {
104
+ return {
105
+ success: true,
106
+ message,
107
+ ...data
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Categorize error by type
113
+ */
114
+ private static categorizeError(error: any): ErrorType {
115
+ if (error instanceof ToolError) {
116
+ return error.type;
117
+ }
118
+
119
+ const errorMessage = error.message?.toLowerCase() || String(error).toLowerCase();
120
+
121
+ // Connection errors
122
+ if (
123
+ errorMessage.includes('econnrefused') ||
124
+ errorMessage.includes('timeout') ||
125
+ errorMessage.includes('connection') ||
126
+ errorMessage.includes('network')
127
+ ) {
128
+ return ErrorType.CONNECTION;
129
+ }
130
+
131
+ // Validation errors
132
+ if (
133
+ errorMessage.includes('invalid') ||
134
+ errorMessage.includes('required') ||
135
+ errorMessage.includes('must be') ||
136
+ errorMessage.includes('validation')
137
+ ) {
138
+ return ErrorType.VALIDATION;
139
+ }
140
+
141
+ // Unreal Engine specific errors
142
+ if (
143
+ errorMessage.includes('unreal') ||
144
+ errorMessage.includes('remote control') ||
145
+ errorMessage.includes('blueprint') ||
146
+ errorMessage.includes('actor') ||
147
+ errorMessage.includes('asset')
148
+ ) {
149
+ return ErrorType.UNREAL_ENGINE;
150
+ }
151
+
152
+ // Parameter errors
153
+ if (
154
+ errorMessage.includes('parameter') ||
155
+ errorMessage.includes('argument') ||
156
+ errorMessage.includes('missing')
157
+ ) {
158
+ return ErrorType.PARAMETER;
159
+ }
160
+
161
+ // Timeout errors
162
+ if (errorMessage.includes('timeout')) {
163
+ return ErrorType.TIMEOUT;
164
+ }
165
+
166
+ return ErrorType.UNKNOWN;
167
+ }
168
+
169
+ /**
170
+ * Get user-friendly error message
171
+ */
172
+ private static getUserFriendlyMessage(type: ErrorType, error: any): string {
173
+ const originalMessage = error.message || String(error);
174
+
175
+ switch (type) {
176
+ case ErrorType.CONNECTION:
177
+ return 'Failed to connect to Unreal Engine. Please ensure Remote Control is enabled and the engine is running.';
178
+
179
+ case ErrorType.VALIDATION:
180
+ return `Invalid input: ${originalMessage}`;
181
+
182
+ case ErrorType.UNREAL_ENGINE:
183
+ return `Unreal Engine error: ${originalMessage}`;
184
+
185
+ case ErrorType.PARAMETER:
186
+ return `Invalid parameters: ${originalMessage}`;
187
+
188
+ case ErrorType.TIMEOUT:
189
+ return 'Operation timed out. Unreal Engine may be busy or unresponsive.';
190
+
191
+ case ErrorType.EXECUTION:
192
+ return `Execution failed: ${originalMessage}`;
193
+
194
+ default:
195
+ return originalMessage;
196
+ }
197
+ }
198
+
199
+ /** Determine if an error is likely retriable */
200
+ private static isRetriable(error: any): boolean {
201
+ try {
202
+ const code = (error?.code || '').toString().toUpperCase();
203
+ const msg = (error?.message || String(error) || '').toLowerCase();
204
+ const status = Number((error?.response?.status));
205
+ if (['ECONNRESET','ECONNREFUSED','ETIMEDOUT','EPIPE'].includes(code)) return true;
206
+ if (/timeout|timed out|network|connection|closed|unavailable|busy|temporar/.test(msg)) return true;
207
+ if (!isNaN(status) && (status === 429 || (status >= 500 && status < 600))) return true;
208
+ } catch {}
209
+ return false;
210
+ }
211
+
212
+ /**
213
+ * Wrap async function with error handling
214
+ */
215
+ static async wrapAsync<T extends BaseToolResponse>(
216
+ toolName: string,
217
+ fn: () => Promise<T>,
218
+ context?: any
219
+ ): Promise<T> {
220
+ try {
221
+ const result = await fn();
222
+
223
+ // Ensure result has success field
224
+ if (typeof result === 'object' && result !== null) {
225
+ if (!('success' in result)) {
226
+ (result as any).success = true;
227
+ }
228
+ }
229
+
230
+ return result;
231
+ } catch (error) {
232
+ return this.createErrorResponse(error, toolName, context) as T;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Validate required parameters
238
+ */
239
+ static validateParams(
240
+ params: any,
241
+ required: string[],
242
+ toolName: string
243
+ ): void {
244
+ if (!params || typeof params !== 'object') {
245
+ throw new ToolError(
246
+ ErrorType.PARAMETER,
247
+ toolName,
248
+ 'Invalid parameters: expected object'
249
+ );
250
+ }
251
+
252
+ for (const field of required) {
253
+ if (!(field in params) || params[field] === undefined || params[field] === null) {
254
+ throw new ToolError(
255
+ ErrorType.PARAMETER,
256
+ toolName,
257
+ `Missing required parameter: ${field}`
258
+ );
259
+ }
260
+
261
+ // Additional validation for common types
262
+ if (field.includes('Path') || field.includes('Name')) {
263
+ if (typeof params[field] !== 'string' || params[field].trim() === '') {
264
+ throw new ToolError(
265
+ ErrorType.PARAMETER,
266
+ toolName,
267
+ `Invalid ${field}: must be a non-empty string`
268
+ );
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Handle Unreal Engine specific errors
276
+ */
277
+ static handleUnrealError(error: any, operation: string): string {
278
+ const errorStr = String(error.message || error).toLowerCase();
279
+
280
+ // Common Unreal errors
281
+ if (errorStr.includes('worldcontext')) {
282
+ return `${operation} completed (WorldContext warnings are normal)`;
283
+ }
284
+
285
+ if (errorStr.includes('does not exist')) {
286
+ return `Asset or object not found for ${operation}`;
287
+ }
288
+
289
+ if (errorStr.includes('access denied') || errorStr.includes('read-only')) {
290
+ return `Permission denied for ${operation}. Check file permissions.`;
291
+ }
292
+
293
+ if (errorStr.includes('already exists')) {
294
+ return `Object already exists for ${operation}`;
295
+ }
296
+
297
+ return `Unreal Engine error during ${operation}: ${error.message || error}`;
298
+ }
299
+
300
+ /**
301
+ * Create operation result with consistent structure
302
+ */
303
+ static createResult<T extends BaseToolResponse>(
304
+ success: boolean,
305
+ message: string,
306
+ data?: Partial<T>
307
+ ): T {
308
+ const result: BaseToolResponse = {
309
+ success,
310
+ message,
311
+ ...(data || {})
312
+ };
313
+
314
+ if (!success && !result.error) {
315
+ result.error = message;
316
+ }
317
+
318
+ return result as T;
319
+ }
320
+ }