@@ -14,6 +14,69 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1414
1515// Using McpServer type from SDK instead of custom interface
1616
17+ // Configuration for LLM parameters - made configurable instead of hardcoded
18+ interface LLMConfig {
19+ maxTokens : number ;
20+ temperature ?: number ;
21+ }
22+
23+ // Default LLM configuration with environment variable overrides
24+ const getLLMConfig = ( ) : LLMConfig => ( {
25+ maxTokens : process . env . XCODEBUILDMCP_LLM_MAX_TOKENS
26+ ? parseInt ( process . env . XCODEBUILDMCP_LLM_MAX_TOKENS , 10 )
27+ : 200 ,
28+ temperature : process . env . XCODEBUILDMCP_LLM_TEMPERATURE
29+ ? parseFloat ( process . env . XCODEBUILDMCP_LLM_TEMPERATURE )
30+ : undefined ,
31+ } ) ;
32+
33+ /**
34+ * Sanitizes user input to prevent injection attacks and ensure safe LLM usage
35+ * @param input The raw user input to sanitize
36+ * @returns Sanitized input safe for LLM processing
37+ */
38+ function sanitizeTaskDescription ( input : string ) : string {
39+ if ( ! input || typeof input !== 'string' ) {
40+ throw new Error ( 'Task description must be a non-empty string' ) ;
41+ }
42+
43+ // Remove control characters and normalize whitespace
44+ let sanitized = input
45+ // eslint-disable-next-line no-control-regex -- Intentional control character removal for security
46+ . replace ( / [ \x00 - \x1F \x7F - \x9F ] / g, '' ) // Remove control characters
47+ . replace ( / \s + / g, ' ' ) // Normalize whitespace
48+ . trim ( ) ;
49+
50+ // Length validation - prevent excessively long inputs
51+ if ( sanitized . length === 0 ) {
52+ throw new Error ( 'Task description cannot be empty after sanitization' ) ;
53+ }
54+
55+ if ( sanitized . length > 2000 ) {
56+ sanitized = sanitized . substring ( 0 , 2000 ) ;
57+ log ( 'warn' , 'Task description truncated to 2000 characters for safety' ) ;
58+ }
59+
60+ // Basic injection prevention - remove potential prompt injection patterns
61+ const suspiciousPatterns = [
62+ / i g n o r e \s + p r e v i o u s \s + i n s t r u c t i o n s / gi,
63+ / f o r g e t \s + e v e r y t h i n g / gi,
64+ / s y s t e m \s * : / gi,
65+ / a s s i s t a n t \s * : / gi,
66+ / y o u \s + a r e \s + n o w / gi,
67+ / a c t \s + a s / gi,
68+ ] ;
69+
70+ for ( const pattern of suspiciousPatterns ) {
71+ if ( pattern . test ( sanitized ) ) {
72+ log ( 'warn' , 'Potentially suspicious pattern detected in task description' ) ;
73+ sanitized = sanitized . replace ( pattern , '[filtered]' ) ;
74+ }
75+ }
76+
77+ return sanitized ;
78+ }
79+
1780// Define schema as ZodObject
1881const discoverToolsSchema = z . object ( {
1982 task_description : z
@@ -47,8 +110,23 @@ export async function discover_toolsLogic(
47110 _executor ?: unknown ,
48111 deps ?: Dependencies ,
49112) : Promise < ToolResponse > {
113+ // Enhanced null safety checks
114+ if ( ! args || typeof args !== 'object' ) {
115+ return createTextResponse ( 'Invalid arguments provided to discover_tools' , true ) ;
116+ }
117+
50118 const { task_description, additive } = args ;
51- log ( 'info' , `Discovering tools for task: ${ task_description } ` ) ;
119+
120+ // Sanitize the task description to prevent injection attacks
121+ let sanitizedTaskDescription : string ;
122+ try {
123+ sanitizedTaskDescription = sanitizeTaskDescription ( task_description ) ;
124+ log ( 'info' , `Discovering tools for task: ${ sanitizedTaskDescription } ` ) ;
125+ } catch ( error ) {
126+ const errorMessage = error instanceof Error ? error . message : 'Invalid task description' ;
127+ log ( 'error' , `Task description sanitization failed: ${ errorMessage } ` ) ;
128+ return createTextResponse ( `Invalid task description: ${ errorMessage } ` , true ) ;
129+ }
52130
53131 try {
54132 // Get the server instance from the global context
@@ -74,10 +152,10 @@ export async function discover_toolsLogic(
74152 deps ?. generateWorkflowDescriptions ?? generateWorkflowDescriptions
75153 ) ( ) ;
76154
77- // 3. Construct the prompt for the LLM
155+ // 3. Construct the prompt for the LLM using sanitized input
78156 const userPrompt = `You are an expert assistant for the XcodeBuildMCP server. Your task is to select the most relevant workflow for a user's Apple development request.
79157
80- The user wants to perform the following task: "${ task_description } "
158+ The user wants to perform the following task: "${ sanitizedTaskDescription } "
81159
82160IMPORTANT: Each workflow represents a complete end-to-end development workflow. Choose ONLY ONE workflow that best matches the user's project type and target platform:
83161
@@ -98,41 +176,79 @@ ${workflowDescriptions}
98176Respond with ONLY a JSON array containing ONE workflow name that best matches the task (e.g., ["simulator-workspace"]).
99177Each workflow contains ALL tools needed for its complete development workflow - no need to combine workflows.` ;
100178
101- // 4. Send sampling request
102- log ( 'debug' , 'Sending sampling request to client LLM' ) ;
179+ // 4. Send sampling request with configurable parameters
180+ const llmConfig = getLLMConfig ( ) ;
181+ log ( 'debug' , `Sending sampling request to client LLM with maxTokens: ${ llmConfig . maxTokens } ` ) ;
103182 if ( ! server . server ?. createMessage ) {
104183 throw new Error ( 'Server does not support message creation' ) ;
105184 }
106- const samplingResult = await server . server . createMessage ( {
185+
186+ const samplingOptions : {
187+ messages : Array < { role : 'user' ; content : { type : 'text' ; text : string } } > ;
188+ maxTokens : number ;
189+ temperature ?: number ;
190+ } = {
107191 messages : [ { role : 'user' , content : { type : 'text' , text : userPrompt } } ] ,
108- maxTokens : 200 ,
109- } ) ;
192+ maxTokens : llmConfig . maxTokens ,
193+ } ;
110194
111- // 5. Parse the response
195+ // Only add temperature if configured
196+ if ( llmConfig . temperature !== undefined ) {
197+ samplingOptions . temperature = llmConfig . temperature ;
198+ }
199+
200+ const samplingResult = await server . server . createMessage ( samplingOptions ) ;
201+
202+ // 5. Parse the response with enhanced null safety checks
112203 let selectedWorkflows : string [ ] = [ ] ;
113204 try {
205+ // Enhanced null safety - check if samplingResult exists and has expected structure
206+ if ( ! samplingResult || typeof samplingResult !== 'object' ) {
207+ throw new Error ( 'Invalid sampling result: null or not an object' ) ;
208+ }
209+
114210 const content = (
115211 samplingResult as {
116- content : Array < { type : 'text' ; text : string } > | { type : 'text' ; text : string } ;
212+ content ? : Array < { type : 'text' ; text : string } > | { type : 'text' ; text : string } | null ;
117213 }
118214 ) . content ;
215+
216+ if ( ! content ) {
217+ throw new Error ( 'No content in sampling response' ) ;
218+ }
219+
119220 let responseText = '' ;
120221
121- // Handle both array and single object content formats
122- if ( Array . isArray ( content ) && content . length > 0 && content [ 0 ] . type === 'text' ) {
123- responseText = content [ 0 ] . text . trim ( ) ;
222+ // Handle both array and single object content formats with enhanced null checks
223+ if ( Array . isArray ( content ) ) {
224+ if ( content . length === 0 ) {
225+ throw new Error ( 'Empty content array in sampling response' ) ;
226+ }
227+ const firstItem = content [ 0 ] ;
228+ if ( ! firstItem || typeof firstItem !== 'object' || firstItem . type !== 'text' ) {
229+ throw new Error ( 'Invalid first content item in array' ) ;
230+ }
231+ if ( ! firstItem . text || typeof firstItem . text !== 'string' ) {
232+ throw new Error ( 'Invalid text content in first array item' ) ;
233+ }
234+ responseText = firstItem . text . trim ( ) ;
124235 } else if (
125236 content &&
126237 typeof content === 'object' &&
127238 'type' in content &&
128239 content . type === 'text' &&
129- 'text' in content
240+ 'text' in content &&
241+ typeof content . text === 'string'
130242 ) {
131- responseText = ( content . text as string ) . trim ( ) ;
243+ responseText = content . text . trim ( ) ;
132244 } else {
133245 throw new Error ( 'Invalid content format in sampling response' ) ;
134246 }
135247
248+ if ( ! responseText ) {
249+ throw new Error ( 'Empty response text after parsing' ) ;
250+ }
251+
136252 log ( 'debug' , `LLM response: ${ responseText } ` ) ;
137253
138254 const parsedResponse : unknown = JSON . parse ( responseText ) ;
@@ -161,24 +277,39 @@ Each workflow contains ALL tools needed for its complete development workflow -
161277 }
162278 } catch ( error ) {
163279 log ( 'error' , `Failed to parse LLM response: ${ error } ` ) ;
164- // Extract the response text for error reporting
280+ // Extract the response text for error reporting with enhanced null safety
165281 let errorResponseText = 'Unknown response format' ;
166282 try {
167- const content = (
168- samplingResult as {
169- content : Array < { type : 'text' ; text : string } > | { type : 'text' ; text : string } ;
283+ if ( samplingResult && typeof samplingResult === 'object' ) {
284+ const content = (
285+ samplingResult as {
286+ content ?:
287+ | Array < { type : 'text' ; text : string } >
288+ | { type : 'text' ; text : string }
289+ | null ;
290+ }
291+ ) . content ;
292+
293+ if ( content && Array . isArray ( content ) && content . length > 0 ) {
294+ const firstItem = content [ 0 ] ;
295+ if (
296+ firstItem &&
297+ typeof firstItem === 'object' &&
298+ firstItem . type === 'text' &&
299+ typeof firstItem . text === 'string'
300+ ) {
301+ errorResponseText = firstItem . text ;
302+ }
303+ } else if (
304+ content &&
305+ typeof content === 'object' &&
306+ 'type' in content &&
307+ content . type === 'text' &&
308+ 'text' in content &&
309+ typeof content . text === 'string'
310+ ) {
311+ errorResponseText = content . text ;
170312 }
171- ) . content ;
172- if ( Array . isArray ( content ) && content . length > 0 && content [ 0 ] . type === 'text' ) {
173- errorResponseText = content [ 0 ] . text ;
174- } else if (
175- content &&
176- typeof content === 'object' &&
177- 'type' in content &&
178- content . type === 'text' &&
179- 'text' in content
180- ) {
181- errorResponseText = content . text as string ;
182313 }
183314 } catch {
184315 // Keep default error message
0 commit comments