listPets
Retrieve a specified number of pets from an API by defining a limit, powered by the OpenAPI to MCP Server for streamlined API interactions.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | How many items to return at one time |
Input Schema (JSON Schema)
{
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"limit": {
"description": "How many items to return at one time",
"type": "integer"
}
},
"type": "object"
}
Implementation Reference
- src/server.ts:105-157 (handler)The handler function for the 'listPets' tool (shared across all dynamic tools). It receives tool parameters, calls executeApiCall to perform the underlying API request, and formats the response as MCP content or throws MCP errors on failure.server.tool( mcpToolDefinition.name, params, // This schema will be visible in the MCP Inspector async (toolParams: any) => { const requestId = 'req-' + Math.random().toString(36).substring(2, 9); console.error(`MCP Tool '${mcpToolDefinition.name}' invoked. Request ID: ${requestId}`); console.error(`Parameters received:`, toolParams); try { // Execute the API call with the provided parameters const result = await executeApiCall(apiCallDetails, toolParams); if (result.success) { console.error(`[Request ID: ${requestId}] Tool '${mcpToolDefinition.name}' executed successfully.`); // Return success response return { content: [ { type: "text", text: JSON.stringify(result.data) } ] }; } else { console.error(`[Request ID: ${requestId}] Tool '${mcpToolDefinition.name}' execution failed: ${result.error}`); // Map API errors to MCP errors let errorCode = ErrorCode.InternalError; let errorMessage = result.error || `API Error ${result.statusCode}`; if (result.statusCode === 400) { errorCode = ErrorCode.InvalidParams; errorMessage = `Invalid parameters: ${result.error}`; } else if (result.statusCode === 404) { errorCode = ErrorCode.InvalidParams; errorMessage = `Resource not found: ${result.error}`; } throw new McpError(errorCode, errorMessage, result.data); } } catch (invocationError: any) { console.error(`[Request ID: ${requestId}] Error invoking tool:`, invocationError); if (invocationError instanceof McpError) { throw invocationError; // Re-throw known MCP errors } throw new McpError( ErrorCode.InternalError, `Internal server error: ${invocationError.message}` ); }
- src/server.ts:49-165 (registration)Registration loop that dynamically registers MCP tools from the processed OpenAPI spec, including the 'listPets' tool based on its operationId.for (const tool of mappedTools) { const { mcpToolDefinition, apiCallDetails } = tool; console.error(`Registering MCP tool: ${mcpToolDefinition.name}`); try { // Convert JSON Schema properties to zod schema const params: any = {}; if (mcpToolDefinition.inputSchema && mcpToolDefinition.inputSchema.properties) { // Loop through all properties and create appropriate Zod schemas based on data type for (const [propName, propSchema] of Object.entries(mcpToolDefinition.inputSchema.properties)) { if (typeof propSchema !== 'object') continue; const description = propSchema.description as string || `Parameter: ${propName}`; const required = mcpToolDefinition.inputSchema.required?.includes(propName) || false; // Map JSON Schema types to Zod schema types let zodSchema; const schemaType = Array.isArray(propSchema.type) ? propSchema.type[0] // If type is an array (for nullable union types), use first type : propSchema.type; // Handle different types with proper Zod schemas switch (schemaType) { case 'integer': zodSchema = z.number().int().describe(description); break; case 'number': zodSchema = z.number().describe(description); break; case 'boolean': zodSchema = z.boolean().describe(description); break; case 'object': // For objects, create a more permissive schema zodSchema = z.object({}).passthrough().describe(description); break; case 'array': // For arrays, allow any array content zodSchema = z.array(z.any()).describe(description); break; case 'string': default: zodSchema = z.string().describe(description); break; } // Make it optional if not required params[propName] = required ? zodSchema : zodSchema.optional(); // Add this for debugging console.error(`Registered parameter ${propName} with type: ${schemaType}, required: ${required}`); } } // Register the tool using proper MCP SDK format server.tool( mcpToolDefinition.name, params, // This schema will be visible in the MCP Inspector async (toolParams: any) => { const requestId = 'req-' + Math.random().toString(36).substring(2, 9); console.error(`MCP Tool '${mcpToolDefinition.name}' invoked. Request ID: ${requestId}`); console.error(`Parameters received:`, toolParams); try { // Execute the API call with the provided parameters const result = await executeApiCall(apiCallDetails, toolParams); if (result.success) { console.error(`[Request ID: ${requestId}] Tool '${mcpToolDefinition.name}' executed successfully.`); // Return success response return { content: [ { type: "text", text: JSON.stringify(result.data) } ] }; } else { console.error(`[Request ID: ${requestId}] Tool '${mcpToolDefinition.name}' execution failed: ${result.error}`); // Map API errors to MCP errors let errorCode = ErrorCode.InternalError; let errorMessage = result.error || `API Error ${result.statusCode}`; if (result.statusCode === 400) { errorCode = ErrorCode.InvalidParams; errorMessage = `Invalid parameters: ${result.error}`; } else if (result.statusCode === 404) { errorCode = ErrorCode.InvalidParams; errorMessage = `Resource not found: ${result.error}`; } throw new McpError(errorCode, errorMessage, result.data); } } catch (invocationError: any) { console.error(`[Request ID: ${requestId}] Error invoking tool:`, invocationError); if (invocationError instanceof McpError) { throw invocationError; // Re-throw known MCP errors } throw new McpError( ErrorCode.InternalError, `Internal server error: ${invocationError.message}` ); } } ); console.error(`Registered Tool: ${mcpToolDefinition.name}`); } catch (registerError) { console.error(`Failed to register tool ${mcpToolDefinition.name}:`, registerError); } }
- src/server.ts:57-102 (schema)Dynamic construction of Zod input schema for tool parameters from the mapped OpenAPI inputSchema JSON Schema, used for validation in MCP tool calls.if (mcpToolDefinition.inputSchema && mcpToolDefinition.inputSchema.properties) { // Loop through all properties and create appropriate Zod schemas based on data type for (const [propName, propSchema] of Object.entries(mcpToolDefinition.inputSchema.properties)) { if (typeof propSchema !== 'object') continue; const description = propSchema.description as string || `Parameter: ${propName}`; const required = mcpToolDefinition.inputSchema.required?.includes(propName) || false; // Map JSON Schema types to Zod schema types let zodSchema; const schemaType = Array.isArray(propSchema.type) ? propSchema.type[0] // If type is an array (for nullable union types), use first type : propSchema.type; // Handle different types with proper Zod schemas switch (schemaType) { case 'integer': zodSchema = z.number().int().describe(description); break; case 'number': zodSchema = z.number().describe(description); break; case 'boolean': zodSchema = z.boolean().describe(description); break; case 'object': // For objects, create a more permissive schema zodSchema = z.object({}).passthrough().describe(description); break; case 'array': // For arrays, allow any array content zodSchema = z.array(z.any()).describe(description); break; case 'string': default: zodSchema = z.string().describe(description); break; } // Make it optional if not required params[propName] = required ? zodSchema : zodSchema.optional(); // Add this for debugging console.error(`Registered parameter ${propName} with type: ${schemaType}, required: ${required}`); } }
- src/apiClient.ts:150-319 (helper)Core helper function that executes the actual HTTP API call for each tool, including 'listPets'. Maps MCP tool inputs to OpenAPI parameters, applies auth/security, makes axios request, and returns result.export async function executeApiCall( details: ApiCallDetails, mcpInput: Record<string, any> // The raw input object from MCP ): Promise<ApiClientResponse> { const { method, pathTemplate, serverUrl, parameters, requestBody, securityRequirements, securitySchemes } = details; // Input validation if (!method) { return { success: false, statusCode: 400, error: 'API call details missing HTTP method' }; } if (!pathTemplate) { return { success: false, statusCode: 400, error: 'API call details missing path template' }; } if (!serverUrl) { return { success: false, statusCode: 400, error: 'API call details missing server URL' }; } // Ensure mcpInput is a valid object if (!mcpInput || typeof mcpInput !== 'object' || Array.isArray(mcpInput)) { console.error(`Invalid input type: ${typeof mcpInput}. Expected an object.`); return { success: false, statusCode: 400, error: 'Invalid input: expected an object' }; } // Normalize the input object to handle different formats // Some MCP clients might send parameters differently const normalizedInput = { ...mcpInput }; let url = `${serverUrl}${pathTemplate}`; const queryParams: Record<string, any> = {}; const headers: Record<string, any> = {}; let body: any = undefined; console.error(`Executing API call for tool: ${details.method} ${details.pathTemplate}`); console.error(`MCP Input received:`, normalizedInput); // Map MCP input back to HTTP request components for (const paramDef of parameters) { const paramName = paramDef.name; const paramValue = normalizedInput[paramName]; if (paramValue !== undefined) { // Only map if present in MCP input // Validate parameter value according to schema if possible if (paramDef.schema) { const validationError = validateParameterValue(paramValue, paramDef); if (validationError) { return { success: false, statusCode: 400, error: `Parameter '${paramName}': ${validationError}` }; } } switch (paramDef.in) { case 'path': url = url.replace(`{${paramName}}`, encodeURIComponent(String(paramValue))); break; case 'query': queryParams[paramName] = paramValue; break; case 'header': headers[paramName] = String(paramValue); break; case 'cookie': // Cookie handling is more complex, often managed by agents or specific header logic console.error(`Cookie parameter '${paramName}' handling not implemented.`); break; } } else if (paramDef.required) { console.error(`Error: Required parameter '${paramName}' missing in MCP input.`); return { success: false, statusCode: 400, error: `Missing required parameter: ${paramName}` }; } } // Map request body if defined and present in input if (requestBody && normalizedInput.requestBody !== undefined) { // Validate request body against schema if available const bodyValidationError = validateRequestBody(normalizedInput.requestBody, requestBody); if (bodyValidationError) { return { success: false, statusCode: 400, error: `Request body validation failed: ${bodyValidationError}` }; } // Assuming the nested 'requestBody' property in mcpInput holds the body body = normalizedInput.requestBody; // Assume application/json for now, get content type from requestBody definition if needed headers['Content-Type'] = 'application/json'; } else if (requestBody?.required) { console.error(`Error: Required requestBody missing in MCP input.`); return { success: false, statusCode: 400, error: `Missing required request body` }; } const requestConfig: AxiosRequestConfig = { method: method as any, // Cast needed for Axios types url: url, params: queryParams, headers: headers, data: body, // Validate status to handle non-2xx as resolved promises validateStatus: (status: number) => status >= 200 && status < 500, // Handle 4xx as well }; // Apply custom headers from configuration if (config.customHeaders && Object.keys(config.customHeaders).length > 0) { requestConfig.headers = { ...requestConfig.headers, ...config.customHeaders }; } // Add X-MCP header unless disabled if (!config.disableXMcp) { requestConfig.headers = { ...requestConfig.headers, 'X-MCP': '1' }; } // Apply security before making the call try { await applySecurity(requestConfig, securityRequirements, securitySchemes); } catch (secErr: any) { console.error("Security application failed:", secErr); return { success: false, statusCode: 401, error: `Security setup failed: ${secErr.message}` }; } console.error(`Making HTTP request:`, { method: requestConfig.method, url: requestConfig.url, params: requestConfig.params, headers: requestConfig.headers, data: requestConfig.data ? '[Request Body Present]' : undefined // Avoid logging sensitive data }); try { const response = await axios(requestConfig); console.error(`API response received: Status ${response.status}`); if (response.status >= 200 && response.status < 300) { return { success: true, statusCode: response.status, data: response.data, }; } else { // Handle 4xx client errors reported by the API console.error(`API returned client error ${response.status}:`, response.data); return { success: false, statusCode: response.status, error: `API Error ${response.status}: ${JSON.stringify(response.data)}`, data: response.data // Optionally include error data }; } } catch (error) { const axiosError = error as AxiosError; console.error(`API call failed: ${axiosError.message}`, axiosError.response?.data || axiosError.code); if (axiosError.response) { // Errors during the request setup or >= 500 if validateStatus wasn't broad enough return { success: false, statusCode: axiosError.response.status || 500, error: `API Error ${axiosError.response.status}: ${JSON.stringify(axiosError.response.data) || axiosError.message}`, data: axiosError.response.data }; } else { // Network error, DNS error, etc. return { success: false, statusCode: 503, // Service Unavailable or similar error: `Network or request setup error: ${axiosError.message}`, }; } } }
- src/mcpMapper.ts:187-445 (helper)Maps OpenAPI operations to MCP tool definitions, creating the mcpToolDefinition (name, description, schemas) and apiCallDetails for 'listPets' and other operations.export function mapOpenApiToMcpTools(openapi: ProcessedOpenAPI): MappedTool[] { const mappedTools: MappedTool[] = []; const globalSecurity = openapi.security || null; // Global security requirements const securitySchemes = openapi.components?.securitySchemes || undefined; // Security scheme definitions if (!openapi.paths) { console.error("OpenAPI spec has no paths defined."); return []; } // Determine the base server URL // Priority: Configured URL > First Server URL > Error/Default let baseServerUrl = config.targetApiBaseUrl; if (!baseServerUrl) { // Extract URL template from servers, defaulting to '/' if not found baseServerUrl = openapi.servers?.[0]?.url ?? '/'; } // Ensure it's not undefined before using replace baseServerUrl = (baseServerUrl || '/').replace(/\/$/, ''); for (const path in openapi.paths) { const pathItem = openapi.paths[path] as OpenAPIV3.PathItemObject; // Assuming dereferenced for (const method in pathItem) { // Check if the method is a valid HTTP method if (!['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'].includes(method.toLowerCase())) { continue; } const operation = pathItem[method as keyof OpenAPIV3.PathItemObject] as OpenAPIV3.OperationObject; if (!operation || typeof operation !== 'object') continue; const operationId = operation.operationId; // --- Filtering --- if (!shouldIncludeOperation(operationId, path, method)) { // If operationId is available, log it for better debugging if (operationId) { console.error(`Skipping operation ${operationId} (${method.toUpperCase()} ${path}) due to filter rules.`); } else { console.error(`Skipping operation ${method.toUpperCase()} ${path} due to filter rules.`); } continue; } // Skip operations without operationId as we need it for the tool name if (!operationId) { console.error(`Skipping operation ${method.toUpperCase()} ${path} due to missing operationId.`); continue; } // --- Mapping --- let toolName = operationId; // Debug logging to identify what summary/description fields are available console.error(`Tool: ${toolName} - Operation description: ${operation.description || 'N/A'}`); console.error(`Tool: ${toolName} - Operation summary: ${operation.summary || 'N/A'}`); console.error(`Tool: ${toolName} - Path summary: ${pathItem.summary || 'N/A'}`); // Check for custom MCP extensions at the operation level first, then path level const operationMcpExtension = (operation as any)['x-mcp']; const pathMcpExtension = (pathItem as any)['x-mcp']; // Priority: Operation-level extension > Path-level extension > Default if (operationMcpExtension && typeof operationMcpExtension === 'object') { if (operationMcpExtension.name && typeof operationMcpExtension.name === 'string') { console.error(`Tool: ${toolName} - Using custom name from operation-level x-mcp extension: ${operationMcpExtension.name}`); toolName = operationMcpExtension.name; } } else if (pathMcpExtension && typeof pathMcpExtension === 'object') { if (pathMcpExtension.name && typeof pathMcpExtension.name === 'string') { console.error(`Tool: ${toolName} - Using custom name from path-level x-mcp extension: ${pathMcpExtension.name}`); toolName = pathMcpExtension.name; } } let toolDescription = operation.description || operation.summary || pathItem.summary || 'No description available.'; // Check for custom description in MCP extension - operation level first, then path level if (operationMcpExtension && typeof operationMcpExtension === 'object') { if (operationMcpExtension.description && typeof operationMcpExtension.description === 'string') { console.error(`Tool: ${toolName} - Using custom description from operation-level x-mcp extension: ${operationMcpExtension.description}`); toolDescription = operationMcpExtension.description; } } else if (pathMcpExtension && typeof pathMcpExtension === 'object') { if (pathMcpExtension.description && typeof pathMcpExtension.description === 'string') { console.error(`Tool: ${toolName} - Using custom description from path-level x-mcp extension: ${pathMcpExtension.description}`); toolDescription = pathMcpExtension.description; } } console.error(`Tool: ${toolName} - Final description used: ${toolDescription}`); // --- Input Schema --- const inputJsonSchema: JSONSchema7 = { type: 'object', properties: {}, required: [], }; const allParameters: OpenAPIV3.ParameterObject[] = [ ...(pathItem.parameters || []), // Parameters defined at path level ...(operation.parameters || []), // Parameters defined at operation level ].filter(isParameterObject); // Ensure they are actual parameter objects // Group parameters by their location (path, query, header, cookie) const parametersByLocation: Record<string, OpenAPIV3.ParameterObject[]> = {}; for (const param of allParameters) { const location = param.in || 'query'; // Default to query if not specified if (!parametersByLocation[location]) { parametersByLocation[location] = []; } parametersByLocation[location].push(param); } // Create separate property for each parameter location group for better organization for (const [location, params] of Object.entries(parametersByLocation)) { // If there are parameters in this location, create a property for them if (params.length > 0 && inputJsonSchema.properties) { // Process each parameter within its location group for (const param of params) { if (param.name && param.schema && inputJsonSchema.properties) { // Debug log to identify potential type issues console.error(`Processing parameter ${param.name} with schema type: ${(param.schema as any).type} format: ${(param.schema as any).format}`); // Convert OpenAPI schema to JSON Schema const paramSchema = openApiSchemaToJsonSchema(param.schema as OpenAPIV3.SchemaObject); if (paramSchema) { // Debug log for converted schema console.error(`Converted schema for ${param.name}: type=${paramSchema.type}, format=${(paramSchema as any).format}`); // Add parameter to properties inputJsonSchema.properties[param.name] = paramSchema; // Add description if available if (param.description) { (inputJsonSchema.properties[param.name] as JSONSchema7).description = param.description; } // Add parameter location metadata as an annotation (inputJsonSchema.properties[param.name] as any)['x-parameter-location'] = location; // Add deprecated flag if needed if (param.deprecated) { (inputJsonSchema.properties[param.name] as any).deprecated = true; } // Add example if available if (param.example !== undefined) { (inputJsonSchema.properties[param.name] as any).example = param.example; } // Handle required parameters if (param.required && inputJsonSchema.required) { inputJsonSchema.required.push(param.name); } // Add any extensions (x-... properties) Object.keys(param).forEach(key => { if (key.startsWith('x-')) { (inputJsonSchema.properties![param.name] as any)[key] = param[key as keyof OpenAPIV3.ParameterObject]; } }); } } } } } // Handle Request Body if (isRequestBodyObject(operation.requestBody)) { // Look for application/json content first, fall back to any available content type const jsonContent = operation.requestBody.content?.['application/json']?.schema; const anyContent = Object.values(operation.requestBody.content || {})[0]?.schema; const requestBodySchema = jsonContent || anyContent; if (isSchemaObject(requestBodySchema)) { // Convert request body schema and add it to input schema const convertedSchema = openApiSchemaToJsonSchema(requestBodySchema); if (convertedSchema && inputJsonSchema.properties) { // Add as 'requestBody' property inputJsonSchema.properties['requestBody'] = convertedSchema; // Mark as required if specified if (operation.requestBody.required && inputJsonSchema.required) { inputJsonSchema.required.push('requestBody'); } // Add description if available if (operation.requestBody.description && inputJsonSchema.properties['requestBody']) { inputJsonSchema.properties['requestBody'].description = operation.requestBody.description; } // Add content type annotation const contentTypes = Object.keys(operation.requestBody.content || {}); if (contentTypes.length > 0) { (inputJsonSchema.properties['requestBody'] as any)['x-content-types'] = contentTypes; } } } } // Remove empty required array if nothing is required if (inputJsonSchema.required?.length === 0) { delete inputJsonSchema.required; } // --- Output Schema (Primary Success Response, e.g., 200) --- let outputJsonSchema: JSONSchema7 | undefined = undefined; const successResponseCode = Object.keys(operation.responses || {}).find(code => code.startsWith('2')); // Find first 2xx response if (successResponseCode) { const response = operation.responses?.[successResponseCode]; if (isResponseObject(response)) { const jsonContent = response.content?.['application/json']?.schema; if (isSchemaObject(jsonContent)) { const tempSchema = openApiSchemaToJsonSchema(jsonContent); outputJsonSchema = tempSchema as JSONSchema7; // Cast to JSONSchema7 since we know it's a schema if(outputJsonSchema && response.description){ outputJsonSchema.description = response.description; // Add response description } } } } // --- Assemble MCP Tool Definition --- const mcpDefinition: McpToolDefinition = { name: toolName, description: toolDescription, inputSchema: inputJsonSchema, outputSchema: outputJsonSchema, // Properly include the outputSchema annotations: { // Add any relevant annotations, e.g., from spec extensions 'x-openapi-path': path, 'x-openapi-method': method.toUpperCase(), } }; // --- Assemble API Call Details --- const apiDetails: ApiCallDetails = { method: method.toUpperCase(), pathTemplate: path, serverUrl: baseServerUrl, // Use the determined base URL parameters: allParameters, // Store original params for mapping back requestBody: isRequestBodyObject(operation.requestBody) ? operation.requestBody : undefined, // Store original body info securityRequirements: operation.security !== undefined ? operation.security : globalSecurity, // Operation security overrides global securitySchemes, // Include security schemes from OpenAPI components }; mappedTools.push({ mcpToolDefinition: mcpDefinition, apiCallDetails: apiDetails }); console.error(`Mapped tool: ${toolName} (${method.toUpperCase()} ${path})`); } } console.error(`Total tools mapped: ${mappedTools.length}`); return mappedTools; }