diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba81622 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +.env +build \ No newline at end of file diff --git a/README.md b/README.md index b83b8fd..7d99ff4 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,18 @@ -# OpenAPI to MCP Generator - -A command-line tool that generates Model Context Protocol (MCP) server code from OpenAPI specifications. This tool helps you quickly create an MCP server that acts as a bridge between LLMs (Large Language Models) and your API. +# OpenAPI to MCP Generator (openapi-mcp-generator) [![npm version](https://img.shields.io/npm/v/openapi-mcp-generator.svg)](https://www.npmjs.com/package/openapi-mcp-generator) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![GitHub repository](https://img.shields.io/badge/GitHub-harsha--iiiv/openapi--mcp--generator-blue.svg)](https://github.com/harsha-iiiv/openapi-mcp-generator) + +A command-line tool that generates a complete, buildable Model Context Protocol (MCP) server project in TypeScript from an OpenAPI specification. This tool helps you quickly create an MCP server using **stdio transport** that acts as a bridge between LLMs (Large Language Models) and your API. ## Features -- **Automatic Tool Generation**: Converts each API endpoint in your OpenAPI spec into an MCP tool -- **Multiple Transport Options**: Supports stdio, WebSocket, and HTTP transport methods -- **Complete Project Setup**: Generates all necessary files to run an MCP server -- **TypeScript Support**: Includes TypeScript definitions and configuration -- **Easy Configuration**: Simple environment-based configuration for the generated server +- **Automatic Tool Generation**: Converts each API operation in your OpenAPI spec into an MCP tool within the generated server. +- **Complete Project Setup**: Generates a full Node.js project structure (`package.json`, `tsconfig.json`, `src/index.ts`, `.gitignore`). +- **TypeScript & Build Ready**: Creates a TypeScript project configured for building into JavaScript for execution. +- **Stdio Transport**: Generated server uses standard input/output for communication with the MCP client (e.g., an LLM). +- **Runtime Argument Validation**: Integrates Zod for validating tool arguments at runtime based on the OpenAPI schema. ## Installation @@ -28,100 +29,101 @@ pnpm add -g openapi-mcp-generator ## Usage -Generate an MCP server from an OpenAPI specification: +Generate an MCP server project from an OpenAPI specification: ```bash -openapi-mcp-generator --openapi path/to/openapi.json --output /Path/to/output +openapi-mcp-generator -i -o [options] ``` ### Command Line Options -| Option | Alias | Description | Default | -|--------|-------|-------------|---------| -| `--openapi` | `-o` | Path or URL to OpenAPI specification | (required) | -| `--output` | `-d` | Output directory for generated files | `./mcp-server` | -| `--name` | `-n` | Name for the MCP server | `openapi-mcp-server` | -| `--version` | `-v` | Version for the MCP server | `1.0.0` | -| `--transport` | `-t` | Transport mechanism (stdio, websocket, http) | `stdio` | -| `--port` | `-p` | Port for websocket or HTTP server | `3000` | -| `--help` | `-h` | Show help information | | +| Option | Alias | Description | Default | +| :--------------- | :---- | :-------------------------------------------------------------------------------------------------- | :------------------------------------------- | +| `--input` | `-i` | Path or URL to the OpenAPI specification file (JSON or YAML). | **(Required)** | +| `--output` | `-o` | Path to the directory where the MCP server project will be created. | **(Required)** | +| `--name` | `-n` | Name for the generated MCP server package (`package.json`). | Derived from OpenAPI title or `my-mcp-server` | +| `--version` | `-v` | Version for the generated MCP server package (`package.json`). | Derived from OpenAPI version or `0.1.0` | +| `--base-url` | `-b` | Base URL for the target API. Required if not in OpenAPI `servers` or if multiple servers specified. | Derived from OpenAPI `servers` if possible | +| `--help` | `-h` | Show help information. | | ### Examples -Generate from a local OpenAPI file: +**Generate from a local OpenAPI file:** ```bash -openapi-mcp-generator --openapi ./specs/petstore.json --output ./petstore-mcp +openapi-mcp-generator -i ./specs/my-api.yaml -o ./my-api-mcp-server ``` -Generate from a remote OpenAPI URL: +**Generate from a remote OpenAPI URL, specifying name and base URL:** ```bash -openapi-mcp-generator --openapi https://petstore3.swagger.io/api/v3/openapi.json --output ./petstore-mcp +openapi-mcp-generator \ + -i https://petstore3.swagger.io/api/v3/openapi.json \ + -o ./petstore-mcp \ + -n petstore-server \ + -b https://petstore3.swagger.io/api/v3 ``` -Specify a WebSocket transport: +## Generated Project Structure + +The tool generates the following structure in the specified output directory: -```bash -openapi-mcp-generator --openapi ./specs/petstore.json --transport websocket --port 8080 ``` - -## Generated Files - -The tool generates the following files in the output directory: - -- `server.js` - The main MCP server implementation -- `package.json` - Dependencies and scripts -- `README.md` - Documentation for the generated server -- `.env.example` - Template for environment variables -- `types.d.ts` - TypeScript type definitions for the API -- `tsconfig.json` - TypeScript configuration +/ +├── .gitignore # Standard Node.js gitignore +├── package.json # Dependencies and scripts for the generated server +├── tsconfig.json # TypeScript configuration for building +└── src/ + └── index.ts # The generated MCP server source code +``` ## Using the Generated Server -After generating your MCP server: +After generating your MCP server project: -1. Navigate to the generated directory: - ```bash - cd my-mcp-server - ``` +1. **Navigate to the generated directory:** + ```bash + cd + # e.g., cd ./petstore-mcp + ``` -2. Install dependencies: - ```bash - npm install - ``` +2. **Install dependencies:** + ```bash + npm install + ``` + This installs `@modelcontextprotocol/sdk`, `axios`, `zod`, `json-schema-to-zod`, and `typescript`. -3. Create an environment file: - ```bash - cp .env.example .env - ``` +3. **(Optional) Implement Authentication/Configuration:** + * The generator sets the `API_BASE_URL` constant in `src/index.ts` if provided or derived. + * You will need to **manually edit `src/index.ts`** to add any necessary API authentication (e.g., setting `Authorization` headers in the `AxiosRequestConfig` using environment variables or other secure methods). Look for the `// Add Authentication logic here if needed` comment within the `CallTool` handler. -4. Edit `.env` to set your API base URL and any required headers: - ``` - API_BASE_URL=https://api.example.com - API_HEADERS=Authorization:Bearer your-token-here - ``` +4. **Build the server code:** + ```bash + npm run build + ``` + This compiles the TypeScript code from `src/` into JavaScript in the `build/` directory. -5. Start the server: - ```bash - npm start - ``` +5. **Start the server:** + ```bash + npm start + ``` + This runs the compiled JavaScript server (`node build/index.js`), which will listen for MCP requests on standard input/output. ## Requirements -- Node.js 16.x or higher -- npm 7.x or higher +- Node.js v18.0.0 or higher (for both the generator and the generated server) ## Contributing -Contributions are welcome! Please feel free to submit a Pull Request. +Contributions are welcome! Please feel free to submit a Pull Request to the [GitHub repository](https://github.com/harsha-iiiv/openapi-mcp-generator). -1. Fork the repository -2. Create your feature branch (`git checkout -b feature/amazing-feature`) -3. Commit your changes (`git commit -m 'Add some amazing feature'`) -4. Push to the branch (`git push origin feature/amazing-feature`) -5. Open a Pull Request +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add some amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request ## License -MIT +[MIT](https://opensource.org/licenses/MIT) +``` \ No newline at end of file diff --git a/examples/petstore-mcp/.gitignore b/examples/petstore-mcp/.gitignore new file mode 100644 index 0000000..43176f1 --- /dev/null +++ b/examples/petstore-mcp/.gitignore @@ -0,0 +1,55 @@ + +# Node dependencies +node_modules +# Build output +dist +build + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory +coverage +*.lcov +.nyc_output + +# Build artifacts +.grunt +bower_components +# build/Release # Covered by build/ above +jspm_packages/ +web_modules/ +.lock-wscript + +# VS Code files +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Caches +.eslintcache +.node_repl_history +.browserslistcache + +# Environment variables +.env +.env.*.local +.env.local diff --git a/examples/petstore-mcp/package.json b/examples/petstore-mcp/package.json new file mode 100644 index 0000000..cbb49da --- /dev/null +++ b/examples/petstore-mcp/package.json @@ -0,0 +1,31 @@ +{ + "name": "swagger-petstore---openapi-3-0", + "version": "1.0.26", + "description": "MCP Server generated from OpenAPI spec for swagger-petstore---openapi-3-0", + "private": true, + "type": "module", + "main": "build/index.js", + "files": [ + "build", + "src" + ], + "scripts": { + "start": "node build/index.js", + "build": "tsc && chmod 755 build/index.js", + "typecheck": "tsc --noEmit", + "prestart": "npm run build" + }, + "engines": { + "node": ">=18.0.0" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.9.0", + "axios": "^1.8.4", + "json-schema-to-zod": "^2.6.1", + "zod": "^3.24.2" + }, + "devDependencies": { + "@types/node": "^18.19.0", + "typescript": "^5.4.5" + } +} diff --git a/examples/petstore-mcp/src/index.ts b/examples/petstore-mcp/src/index.ts new file mode 100644 index 0000000..1ec7e71 --- /dev/null +++ b/examples/petstore-mcp/src/index.ts @@ -0,0 +1,2219 @@ + +// Generated by openapi-to-mcp-generator for swagger-petstore---openapi-3-0 v1.0.26 +// Source OpenAPI spec: https://petstore3.swagger.io/api/v3/openapi.json +// Generation date: 2025-04-12T09:29:22.152Z + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; + +// Import Schemas and Types from /types subpath with .js extension +import { + CallToolRequestSchema, + ListToolsRequestSchema, + type Tool, + type CallToolResult, + type CallToolRequest // Added type for the request parameter +} from "@modelcontextprotocol/sdk/types.js"; + +// Zod for runtime validation +import { z, ZodError } from 'zod'; +// Library to convert JSON Schema to Zod schema string at runtime +import { jsonSchemaToZod } from 'json-schema-to-zod'; + +// Define JsonObject locally as a utility type +type JsonObject = Record; + +import axios, { type AxiosRequestConfig, type AxiosError } from 'axios'; + +// --- Server Configuration --- +const SERVER_NAME = "swagger-petstore---openapi-3-0"; +const SERVER_VERSION = "1.0.26"; +const API_BASE_URL = "https://petstore3.swagger.io/api/v3"; + +// --- Server Instance --- +const server = new Server( + { + name: SERVER_NAME, + version: SERVER_VERSION + }, + { + capabilities: { + tools: {} + } + } +); + +// --- Tool Definitions (for ListTools response) --- +// Corrected: Use Tool[] type +const toolsList: Tool[] = [ + + // Tool: updatePet (PUT /pet) + { + name: "updatePet", + description: `Update an existing pet by Id.`, + inputSchema: { + "type": "object", + "properties": { + "requestBody": { + "required": [ + "name", + "photoUrls" + ], + "type": "object", + "properties": { + "id": { + "type": "number", + "format": "int64" + }, + "name": { + "type": "string" + }, + "category": { + "type": "object", + "properties": { + "id": { + "type": "number", + "format": "int64" + }, + "name": { + "type": "string" + } + } + }, + "photoUrls": { + "type": "array", + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "number", + "format": "int64" + }, + "name": { + "type": "string" + } + } + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "description": "Update an existent pet in the store" + } + }, + "required": [ + "requestBody" + ] + } + }, + // Tool: addPet (POST /pet) + { + name: "addPet", + description: `Add a new pet to the store.`, + inputSchema: { + "type": "object", + "properties": { + "requestBody": { + "required": [ + "name", + "photoUrls" + ], + "type": "object", + "properties": { + "id": { + "type": "number", + "format": "int64" + }, + "name": { + "type": "string" + }, + "category": { + "type": "object", + "properties": { + "id": { + "type": "number", + "format": "int64" + }, + "name": { + "type": "string" + } + } + }, + "photoUrls": { + "type": "array", + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "number", + "format": "int64" + }, + "name": { + "type": "string" + } + } + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "description": "Create a new pet in the store" + } + }, + "required": [ + "requestBody" + ] + } + }, + // Tool: findPetsByStatus (GET /pet/findByStatus) + { + name: "findPetsByStatus", + description: `Multiple status values can be provided with comma separated strings.`, + inputSchema: { + "type": "object", + "properties": { + "status": { + "type": "string", + "default": "available", + "enum": [ + "available", + "pending", + "sold" + ], + "description": "Status values that need to be considered for filter" + } + } + } + }, + // Tool: findPetsByTags (GET /pet/findByTags) + { + name: "findPetsByTags", + description: `Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.`, + inputSchema: { + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags to filter by" + } + } + } + }, + // Tool: getPetById (GET /pet/{petId}) + { + name: "getPetById", + description: `Returns a single pet.`, + inputSchema: { + "type": "object", + "properties": { + "petId": { + "type": "number", + "format": "int64", + "description": "ID of pet to return" + } + }, + "required": [ + "petId" + ] + } + }, + // Tool: updatePetWithForm (POST /pet/{petId}) + { + name: "updatePetWithForm", + description: `Updates a pet resource based on the form data.`, + inputSchema: { + "type": "object", + "properties": { + "petId": { + "type": "number", + "format": "int64", + "description": "ID of pet that needs to be updated" + }, + "name": { + "type": "string", + "description": "Name of pet that needs to be updated" + }, + "status": { + "type": "string", + "description": "Status of pet that needs to be updated" + } + }, + "required": [ + "petId" + ] + } + }, + // Tool: deletePet (DELETE /pet/{petId}) + { + name: "deletePet", + description: `Delete a pet.`, + inputSchema: { + "type": "object", + "properties": { + "api_key": { + "type": "string" + }, + "petId": { + "type": "number", + "format": "int64", + "description": "Pet id to delete" + } + }, + "required": [ + "petId" + ] + } + }, + // Tool: uploadFile (POST /pet/{petId}/uploadImage) + { + name: "uploadFile", + description: `Upload image of the pet.`, + inputSchema: { + "type": "object", + "properties": { + "petId": { + "type": "number", + "format": "int64", + "description": "ID of pet to update" + }, + "additionalMetadata": { + "type": "string", + "description": "Additional Metadata" + }, + "requestBody": { + "type": "string", + "description": "Request body (content type: application/octet-stream)" + } + }, + "required": [ + "petId" + ] + } + }, + // Tool: getInventory (GET /store/inventory) + { + name: "getInventory", + description: `Returns a map of status codes to quantities.`, + inputSchema: { + "type": "object", + "properties": {} + } + }, + // Tool: placeOrder (POST /store/order) + { + name: "placeOrder", + description: `Place a new order in the store.`, + inputSchema: { + "type": "object", + "properties": { + "requestBody": { + "type": "object", + "properties": { + "id": { + "type": "number", + "format": "int64" + }, + "petId": { + "type": "number", + "format": "int64" + }, + "quantity": { + "type": "number", + "format": "int32" + }, + "shipDate": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "Order Status", + "enum": [ + "placed", + "approved", + "delivered" + ] + }, + "complete": { + "type": "boolean" + } + }, + "description": "The JSON request body." + } + } + } + }, + // Tool: getOrderById (GET /store/order/{orderId}) + { + name: "getOrderById", + description: `For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.`, + inputSchema: { + "type": "object", + "properties": { + "orderId": { + "type": "number", + "format": "int64", + "description": "ID of order that needs to be fetched" + } + }, + "required": [ + "orderId" + ] + } + }, + // Tool: deleteOrder (DELETE /store/order/{orderId}) + { + name: "deleteOrder", + description: `For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors.`, + inputSchema: { + "type": "object", + "properties": { + "orderId": { + "type": "number", + "format": "int64", + "description": "ID of the order that needs to be deleted" + } + }, + "required": [ + "orderId" + ] + } + }, + // Tool: createUser (POST /user) + { + name: "createUser", + description: `This can only be done by the logged in user.`, + inputSchema: { + "type": "object", + "properties": { + "requestBody": { + "type": "object", + "properties": { + "id": { + "type": "number", + "format": "int64" + }, + "username": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "userStatus": { + "type": "number", + "description": "User Status", + "format": "int32" + } + }, + "description": "Created user object" + } + } + } + }, + // Tool: createUsersWithListInput (POST /user/createWithList) + { + name: "createUsersWithListInput", + description: `Creates list of users with given input array.`, + inputSchema: { + "type": "object", + "properties": { + "requestBody": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "number", + "format": "int64" + }, + "username": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "userStatus": { + "type": "number", + "description": "User Status", + "format": "int32" + } + } + }, + "description": "The JSON request body." + } + } + } + }, + // Tool: loginUser (GET /user/login) + { + name: "loginUser", + description: `Log into the system.`, + inputSchema: { + "type": "object", + "properties": { + "username": { + "type": "string", + "description": "The user name for login" + }, + "password": { + "type": "string", + "description": "The password for login in clear text" + } + } + } + }, + // Tool: logoutUser (GET /user/logout) + { + name: "logoutUser", + description: `Log user out of the system.`, + inputSchema: { + "type": "object", + "properties": {} + } + }, + // Tool: getUserByName (GET /user/{username}) + { + name: "getUserByName", + description: `Get user detail based on username.`, + inputSchema: { + "type": "object", + "properties": { + "username": { + "type": "string", + "description": "The name that needs to be fetched. Use user1 for testing" + } + }, + "required": [ + "username" + ] + } + }, + // Tool: updateUser (PUT /user/{username}) + { + name: "updateUser", + description: `This can only be done by the logged in user.`, + inputSchema: { + "type": "object", + "properties": { + "username": { + "type": "string", + "description": "name that need to be deleted" + }, + "requestBody": { + "type": "object", + "properties": { + "id": { + "type": "number", + "format": "int64" + }, + "username": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "userStatus": { + "type": "number", + "description": "User Status", + "format": "int32" + } + }, + "description": "Update an existent user in the store" + } + }, + "required": [ + "username" + ] + } + }, + // Tool: deleteUser (DELETE /user/{username}) + { + name: "deleteUser", + description: `This can only be done by the logged in user.`, + inputSchema: { + "type": "object", + "properties": { + "username": { + "type": "string", + "description": "The name that needs to be deleted" + } + }, + "required": [ + "username" + ] + } + }, +]; + +// --- Request Handlers --- + +// 1. List Available Tools Handler +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: toolsList, + }; +}); + +// 2. Call Tool Handler +// Corrected: Added explicit type for 'request' parameter +server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest): Promise => { + const { name: toolName, arguments: toolArgs } = request.params; + + const toolDefinition = toolsList.find(t => t.name === toolName); + + if (!toolDefinition) { + console.error(`Error: Received request for unknown tool: ${toolName}`); + return { content: [{ type: "text", text: `Error: Unknown tool requested: ${toolName}` }] }; + } + + // --- Tool Execution Logic --- + + // Handler for tool: updatePet + if (toolName === "updatePet") { + try { + + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(`Arguments validated successfully for tool '${toolName}'.`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(`Unexpected error during argument validation setup for tool '${toolName}':`, error); + return { content: [{ type: 'text', text: `Internal server error during argument validation setup for tool '${toolName}'.` }] }; + } + } + // --- End Argument Validation --- + + // --- API Call Preparation --- + let urlPath = "/pet"; + if (urlPath.includes('{')) { throw new Error(`Validation passed but failed to resolve path parameters in URL: ${urlPath}. Check schema/validation logic.`); } + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + const queryParams: Record = {}; + + const headers: Record = { 'Accept': 'application/json' }; + // Set Content-Type based on OpenAPI spec (or fallback) + if (typeof validatedArgs?.['requestBody'] !== 'undefined') { headers['content-type'] = 'application/json'; } + + let requestBodyData: any = undefined; + if (validatedArgs && typeof validatedArgs['requestBody'] !== 'undefined') { + requestBodyData = validatedArgs['requestBody']; + } + // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "PUT", + url: requestUrl, + params: queryParams, + headers: headers, + data: requestBodyData, // Pass the prepared request body data // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = `(Status: ${response.status} - No body content)`; + } + return { content: [ { type: "text", text: `API Response (Status: ${response.status}):\n${responseText}` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = `Error executing tool '${toolName}': ${error.message}`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(`Error during execution of tool '${toolName}':`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + } else + // Handler for tool: addPet + if (toolName === "addPet") { + try { + + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(`Arguments validated successfully for tool '${toolName}'.`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(`Unexpected error during argument validation setup for tool '${toolName}':`, error); + return { content: [{ type: 'text', text: `Internal server error during argument validation setup for tool '${toolName}'.` }] }; + } + } + // --- End Argument Validation --- + + // --- API Call Preparation --- + let urlPath = "/pet"; + if (urlPath.includes('{')) { throw new Error(`Validation passed but failed to resolve path parameters in URL: ${urlPath}. Check schema/validation logic.`); } + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + const queryParams: Record = {}; + + const headers: Record = { 'Accept': 'application/json' }; + // Set Content-Type based on OpenAPI spec (or fallback) + if (typeof validatedArgs?.['requestBody'] !== 'undefined') { headers['content-type'] = 'application/json'; } + + let requestBodyData: any = undefined; + if (validatedArgs && typeof validatedArgs['requestBody'] !== 'undefined') { + requestBodyData = validatedArgs['requestBody']; + } + // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "POST", + url: requestUrl, + params: queryParams, + headers: headers, + data: requestBodyData, // Pass the prepared request body data // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = `(Status: ${response.status} - No body content)`; + } + return { content: [ { type: "text", text: `API Response (Status: ${response.status}):\n${responseText}` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = `Error executing tool '${toolName}': ${error.message}`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(`Error during execution of tool '${toolName}':`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + } else + // Handler for tool: findPetsByStatus + if (toolName === "findPetsByStatus") { + try { + + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(`Arguments validated successfully for tool '${toolName}'.`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(`Unexpected error during argument validation setup for tool '${toolName}':`, error); + return { content: [{ type: 'text', text: `Internal server error during argument validation setup for tool '${toolName}'.` }] }; + } + } + // --- End Argument Validation --- + + // --- API Call Preparation --- + let urlPath = "/pet/findByStatus"; + if (urlPath.includes('{')) { throw new Error(`Validation passed but failed to resolve path parameters in URL: ${urlPath}. Check schema/validation logic.`); } + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + const queryParams: Record = {}; + const status_val = validatedArgs['status']; + if (typeof status_val !== 'undefined' && status_val !== null) queryParams['status'] = status_val; + + const headers: Record = { 'Accept': 'application/json' }; + + // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "GET", + url: requestUrl, + params: queryParams, + headers: headers, + // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = `(Status: ${response.status} - No body content)`; + } + return { content: [ { type: "text", text: `API Response (Status: ${response.status}):\n${responseText}` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = `Error executing tool '${toolName}': ${error.message}`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(`Error during execution of tool '${toolName}':`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + } else + // Handler for tool: findPetsByTags + if (toolName === "findPetsByTags") { + try { + + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(`Arguments validated successfully for tool '${toolName}'.`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(`Unexpected error during argument validation setup for tool '${toolName}':`, error); + return { content: [{ type: 'text', text: `Internal server error during argument validation setup for tool '${toolName}'.` }] }; + } + } + // --- End Argument Validation --- + + // --- API Call Preparation --- + let urlPath = "/pet/findByTags"; + if (urlPath.includes('{')) { throw new Error(`Validation passed but failed to resolve path parameters in URL: ${urlPath}. Check schema/validation logic.`); } + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + const queryParams: Record = {}; + const tags_val = validatedArgs['tags']; + if (typeof tags_val !== 'undefined' && tags_val !== null) queryParams['tags'] = tags_val; + + const headers: Record = { 'Accept': 'application/json' }; + + // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "GET", + url: requestUrl, + params: queryParams, + headers: headers, + // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = `(Status: ${response.status} - No body content)`; + } + return { content: [ { type: "text", text: `API Response (Status: ${response.status}):\n${responseText}` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = `Error executing tool '${toolName}': ${error.message}`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(`Error during execution of tool '${toolName}':`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + } else + // Handler for tool: getPetById + if (toolName === "getPetById") { + try { + + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(`Arguments validated successfully for tool '${toolName}'.`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(`Unexpected error during argument validation setup for tool '${toolName}':`, error); + return { content: [{ type: 'text', text: `Internal server error during argument validation setup for tool '${toolName}'.` }] }; + } + } + // --- End Argument Validation --- + + // --- API Call Preparation --- + let urlPath = "/pet/{petId}"; + const petId_val = validatedArgs['petId']; + if (typeof petId_val !== 'undefined' && petId_val !== null) { urlPath = urlPath.replace("{petId}", encodeURIComponent(String(petId_val))); } + if (urlPath.includes('{')) { throw new Error(`Validation passed but failed to resolve path parameters in URL: ${urlPath}. Check schema/validation logic.`); } + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + const queryParams: Record = {}; + + const headers: Record = { 'Accept': 'application/json' }; + + // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "GET", + url: requestUrl, + params: queryParams, + headers: headers, + // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = `(Status: ${response.status} - No body content)`; + } + return { content: [ { type: "text", text: `API Response (Status: ${response.status}):\n${responseText}` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = `Error executing tool '${toolName}': ${error.message}`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(`Error during execution of tool '${toolName}':`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + } else + // Handler for tool: updatePetWithForm + if (toolName === "updatePetWithForm") { + try { + + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(`Arguments validated successfully for tool '${toolName}'.`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(`Unexpected error during argument validation setup for tool '${toolName}':`, error); + return { content: [{ type: 'text', text: `Internal server error during argument validation setup for tool '${toolName}'.` }] }; + } + } + // --- End Argument Validation --- + + // --- API Call Preparation --- + let urlPath = "/pet/{petId}"; + const petId_val = validatedArgs['petId']; + if (typeof petId_val !== 'undefined' && petId_val !== null) { urlPath = urlPath.replace("{petId}", encodeURIComponent(String(petId_val))); } + if (urlPath.includes('{')) { throw new Error(`Validation passed but failed to resolve path parameters in URL: ${urlPath}. Check schema/validation logic.`); } + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + const queryParams: Record = {}; + const name_val = validatedArgs['name']; + if (typeof name_val !== 'undefined' && name_val !== null) queryParams['name'] = name_val; + const status_val = validatedArgs['status']; + if (typeof status_val !== 'undefined' && status_val !== null) queryParams['status'] = status_val; + + const headers: Record = { 'Accept': 'application/json' }; + + // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "POST", + url: requestUrl, + params: queryParams, + headers: headers, + // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = `(Status: ${response.status} - No body content)`; + } + return { content: [ { type: "text", text: `API Response (Status: ${response.status}):\n${responseText}` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = `Error executing tool '${toolName}': ${error.message}`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(`Error during execution of tool '${toolName}':`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + } else + // Handler for tool: deletePet + if (toolName === "deletePet") { + try { + + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(`Arguments validated successfully for tool '${toolName}'.`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(`Unexpected error during argument validation setup for tool '${toolName}':`, error); + return { content: [{ type: 'text', text: `Internal server error during argument validation setup for tool '${toolName}'.` }] }; + } + } + // --- End Argument Validation --- + + // --- API Call Preparation --- + let urlPath = "/pet/{petId}"; + const petId_val = validatedArgs['petId']; + if (typeof petId_val !== 'undefined' && petId_val !== null) { urlPath = urlPath.replace("{petId}", encodeURIComponent(String(petId_val))); } + if (urlPath.includes('{')) { throw new Error(`Validation passed but failed to resolve path parameters in URL: ${urlPath}. Check schema/validation logic.`); } + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + const queryParams: Record = {}; + + const headers: Record = { 'Accept': 'application/json' }; + const api_key_val = validatedArgs['api_key']; + if (typeof api_key_val !== 'undefined' && api_key_val !== null) headers['api_key'] = String(api_key_val); + + // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "DELETE", + url: requestUrl, + params: queryParams, + headers: headers, + // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = `(Status: ${response.status} - No body content)`; + } + return { content: [ { type: "text", text: `API Response (Status: ${response.status}):\n${responseText}` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = `Error executing tool '${toolName}': ${error.message}`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(`Error during execution of tool '${toolName}':`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + } else + // Handler for tool: uploadFile + if (toolName === "uploadFile") { + try { + + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(`Arguments validated successfully for tool '${toolName}'.`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(`Unexpected error during argument validation setup for tool '${toolName}':`, error); + return { content: [{ type: 'text', text: `Internal server error during argument validation setup for tool '${toolName}'.` }] }; + } + } + // --- End Argument Validation --- + + // --- API Call Preparation --- + let urlPath = "/pet/{petId}/uploadImage"; + const petId_val = validatedArgs['petId']; + if (typeof petId_val !== 'undefined' && petId_val !== null) { urlPath = urlPath.replace("{petId}", encodeURIComponent(String(petId_val))); } + if (urlPath.includes('{')) { throw new Error(`Validation passed but failed to resolve path parameters in URL: ${urlPath}. Check schema/validation logic.`); } + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + const queryParams: Record = {}; + const additionalMetadata_val = validatedArgs['additionalMetadata']; + if (typeof additionalMetadata_val !== 'undefined' && additionalMetadata_val !== null) queryParams['additionalMetadata'] = additionalMetadata_val; + + const headers: Record = { 'Accept': 'application/json' }; + // Set Content-Type based on OpenAPI spec (or fallback) + if (typeof validatedArgs?.['requestBody'] !== 'undefined') { headers['content-type'] = 'application/octet-stream'; } + + let requestBodyData: any = undefined; + if (validatedArgs && typeof validatedArgs['requestBody'] !== 'undefined') { + requestBodyData = validatedArgs['requestBody']; + } + // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "POST", + url: requestUrl, + params: queryParams, + headers: headers, + data: requestBodyData, // Pass the prepared request body data // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = `(Status: ${response.status} - No body content)`; + } + return { content: [ { type: "text", text: `API Response (Status: ${response.status}):\n${responseText}` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = `Error executing tool '${toolName}': ${error.message}`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(`Error during execution of tool '${toolName}':`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + } else + // Handler for tool: getInventory + if (toolName === "getInventory") { + try { + + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(`Arguments validated successfully for tool '${toolName}'.`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(`Unexpected error during argument validation setup for tool '${toolName}':`, error); + return { content: [{ type: 'text', text: `Internal server error during argument validation setup for tool '${toolName}'.` }] }; + } + } + // --- End Argument Validation --- + + // --- API Call Preparation --- + let urlPath = "/store/inventory"; + if (urlPath.includes('{')) { throw new Error(`Validation passed but failed to resolve path parameters in URL: ${urlPath}. Check schema/validation logic.`); } + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + const queryParams: Record = {}; + + const headers: Record = { 'Accept': 'application/json' }; + + // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "GET", + url: requestUrl, + params: queryParams, + headers: headers, + // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = `(Status: ${response.status} - No body content)`; + } + return { content: [ { type: "text", text: `API Response (Status: ${response.status}):\n${responseText}` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = `Error executing tool '${toolName}': ${error.message}`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(`Error during execution of tool '${toolName}':`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + } else + // Handler for tool: placeOrder + if (toolName === "placeOrder") { + try { + + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(`Arguments validated successfully for tool '${toolName}'.`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(`Unexpected error during argument validation setup for tool '${toolName}':`, error); + return { content: [{ type: 'text', text: `Internal server error during argument validation setup for tool '${toolName}'.` }] }; + } + } + // --- End Argument Validation --- + + // --- API Call Preparation --- + let urlPath = "/store/order"; + if (urlPath.includes('{')) { throw new Error(`Validation passed but failed to resolve path parameters in URL: ${urlPath}. Check schema/validation logic.`); } + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + const queryParams: Record = {}; + + const headers: Record = { 'Accept': 'application/json' }; + // Set Content-Type based on OpenAPI spec (or fallback) + if (typeof validatedArgs?.['requestBody'] !== 'undefined') { headers['content-type'] = 'application/json'; } + + let requestBodyData: any = undefined; + if (validatedArgs && typeof validatedArgs['requestBody'] !== 'undefined') { + requestBodyData = validatedArgs['requestBody']; + } + // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "POST", + url: requestUrl, + params: queryParams, + headers: headers, + data: requestBodyData, // Pass the prepared request body data // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = `(Status: ${response.status} - No body content)`; + } + return { content: [ { type: "text", text: `API Response (Status: ${response.status}):\n${responseText}` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = `Error executing tool '${toolName}': ${error.message}`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(`Error during execution of tool '${toolName}':`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + } else + // Handler for tool: getOrderById + if (toolName === "getOrderById") { + try { + + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(`Arguments validated successfully for tool '${toolName}'.`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(`Unexpected error during argument validation setup for tool '${toolName}':`, error); + return { content: [{ type: 'text', text: `Internal server error during argument validation setup for tool '${toolName}'.` }] }; + } + } + // --- End Argument Validation --- + + // --- API Call Preparation --- + let urlPath = "/store/order/{orderId}"; + const orderId_val = validatedArgs['orderId']; + if (typeof orderId_val !== 'undefined' && orderId_val !== null) { urlPath = urlPath.replace("{orderId}", encodeURIComponent(String(orderId_val))); } + if (urlPath.includes('{')) { throw new Error(`Validation passed but failed to resolve path parameters in URL: ${urlPath}. Check schema/validation logic.`); } + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + const queryParams: Record = {}; + + const headers: Record = { 'Accept': 'application/json' }; + + // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "GET", + url: requestUrl, + params: queryParams, + headers: headers, + // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = `(Status: ${response.status} - No body content)`; + } + return { content: [ { type: "text", text: `API Response (Status: ${response.status}):\n${responseText}` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = `Error executing tool '${toolName}': ${error.message}`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(`Error during execution of tool '${toolName}':`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + } else + // Handler for tool: deleteOrder + if (toolName === "deleteOrder") { + try { + + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(`Arguments validated successfully for tool '${toolName}'.`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(`Unexpected error during argument validation setup for tool '${toolName}':`, error); + return { content: [{ type: 'text', text: `Internal server error during argument validation setup for tool '${toolName}'.` }] }; + } + } + // --- End Argument Validation --- + + // --- API Call Preparation --- + let urlPath = "/store/order/{orderId}"; + const orderId_val = validatedArgs['orderId']; + if (typeof orderId_val !== 'undefined' && orderId_val !== null) { urlPath = urlPath.replace("{orderId}", encodeURIComponent(String(orderId_val))); } + if (urlPath.includes('{')) { throw new Error(`Validation passed but failed to resolve path parameters in URL: ${urlPath}. Check schema/validation logic.`); } + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + const queryParams: Record = {}; + + const headers: Record = { 'Accept': 'application/json' }; + + // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "DELETE", + url: requestUrl, + params: queryParams, + headers: headers, + // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = `(Status: ${response.status} - No body content)`; + } + return { content: [ { type: "text", text: `API Response (Status: ${response.status}):\n${responseText}` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = `Error executing tool '${toolName}': ${error.message}`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(`Error during execution of tool '${toolName}':`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + } else + // Handler for tool: createUser + if (toolName === "createUser") { + try { + + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(`Arguments validated successfully for tool '${toolName}'.`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(`Unexpected error during argument validation setup for tool '${toolName}':`, error); + return { content: [{ type: 'text', text: `Internal server error during argument validation setup for tool '${toolName}'.` }] }; + } + } + // --- End Argument Validation --- + + // --- API Call Preparation --- + let urlPath = "/user"; + if (urlPath.includes('{')) { throw new Error(`Validation passed but failed to resolve path parameters in URL: ${urlPath}. Check schema/validation logic.`); } + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + const queryParams: Record = {}; + + const headers: Record = { 'Accept': 'application/json' }; + // Set Content-Type based on OpenAPI spec (or fallback) + if (typeof validatedArgs?.['requestBody'] !== 'undefined') { headers['content-type'] = 'application/json'; } + + let requestBodyData: any = undefined; + if (validatedArgs && typeof validatedArgs['requestBody'] !== 'undefined') { + requestBodyData = validatedArgs['requestBody']; + } + // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "POST", + url: requestUrl, + params: queryParams, + headers: headers, + data: requestBodyData, // Pass the prepared request body data // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = `(Status: ${response.status} - No body content)`; + } + return { content: [ { type: "text", text: `API Response (Status: ${response.status}):\n${responseText}` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = `Error executing tool '${toolName}': ${error.message}`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(`Error during execution of tool '${toolName}':`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + } else + // Handler for tool: createUsersWithListInput + if (toolName === "createUsersWithListInput") { + try { + + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(`Arguments validated successfully for tool '${toolName}'.`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(`Unexpected error during argument validation setup for tool '${toolName}':`, error); + return { content: [{ type: 'text', text: `Internal server error during argument validation setup for tool '${toolName}'.` }] }; + } + } + // --- End Argument Validation --- + + // --- API Call Preparation --- + let urlPath = "/user/createWithList"; + if (urlPath.includes('{')) { throw new Error(`Validation passed but failed to resolve path parameters in URL: ${urlPath}. Check schema/validation logic.`); } + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + const queryParams: Record = {}; + + const headers: Record = { 'Accept': 'application/json' }; + // Set Content-Type based on OpenAPI spec (or fallback) + if (typeof validatedArgs?.['requestBody'] !== 'undefined') { headers['content-type'] = 'application/json'; } + + let requestBodyData: any = undefined; + if (validatedArgs && typeof validatedArgs['requestBody'] !== 'undefined') { + requestBodyData = validatedArgs['requestBody']; + } + // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "POST", + url: requestUrl, + params: queryParams, + headers: headers, + data: requestBodyData, // Pass the prepared request body data // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = `(Status: ${response.status} - No body content)`; + } + return { content: [ { type: "text", text: `API Response (Status: ${response.status}):\n${responseText}` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = `Error executing tool '${toolName}': ${error.message}`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(`Error during execution of tool '${toolName}':`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + } else + // Handler for tool: loginUser + if (toolName === "loginUser") { + try { + + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(`Arguments validated successfully for tool '${toolName}'.`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(`Unexpected error during argument validation setup for tool '${toolName}':`, error); + return { content: [{ type: 'text', text: `Internal server error during argument validation setup for tool '${toolName}'.` }] }; + } + } + // --- End Argument Validation --- + + // --- API Call Preparation --- + let urlPath = "/user/login"; + if (urlPath.includes('{')) { throw new Error(`Validation passed but failed to resolve path parameters in URL: ${urlPath}. Check schema/validation logic.`); } + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + const queryParams: Record = {}; + const username_val = validatedArgs['username']; + if (typeof username_val !== 'undefined' && username_val !== null) queryParams['username'] = username_val; + const password_val = validatedArgs['password']; + if (typeof password_val !== 'undefined' && password_val !== null) queryParams['password'] = password_val; + + const headers: Record = { 'Accept': 'application/json' }; + + // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "GET", + url: requestUrl, + params: queryParams, + headers: headers, + // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = `(Status: ${response.status} - No body content)`; + } + return { content: [ { type: "text", text: `API Response (Status: ${response.status}):\n${responseText}` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = `Error executing tool '${toolName}': ${error.message}`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(`Error during execution of tool '${toolName}':`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + } else + // Handler for tool: logoutUser + if (toolName === "logoutUser") { + try { + + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(`Arguments validated successfully for tool '${toolName}'.`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(`Unexpected error during argument validation setup for tool '${toolName}':`, error); + return { content: [{ type: 'text', text: `Internal server error during argument validation setup for tool '${toolName}'.` }] }; + } + } + // --- End Argument Validation --- + + // --- API Call Preparation --- + let urlPath = "/user/logout"; + if (urlPath.includes('{')) { throw new Error(`Validation passed but failed to resolve path parameters in URL: ${urlPath}. Check schema/validation logic.`); } + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + const queryParams: Record = {}; + + const headers: Record = { 'Accept': 'application/json' }; + + // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "GET", + url: requestUrl, + params: queryParams, + headers: headers, + // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = `(Status: ${response.status} - No body content)`; + } + return { content: [ { type: "text", text: `API Response (Status: ${response.status}):\n${responseText}` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = `Error executing tool '${toolName}': ${error.message}`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(`Error during execution of tool '${toolName}':`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + } else + // Handler for tool: getUserByName + if (toolName === "getUserByName") { + try { + + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(`Arguments validated successfully for tool '${toolName}'.`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(`Unexpected error during argument validation setup for tool '${toolName}':`, error); + return { content: [{ type: 'text', text: `Internal server error during argument validation setup for tool '${toolName}'.` }] }; + } + } + // --- End Argument Validation --- + + // --- API Call Preparation --- + let urlPath = "/user/{username}"; + const username_val = validatedArgs['username']; + if (typeof username_val !== 'undefined' && username_val !== null) { urlPath = urlPath.replace("{username}", encodeURIComponent(String(username_val))); } + if (urlPath.includes('{')) { throw new Error(`Validation passed but failed to resolve path parameters in URL: ${urlPath}. Check schema/validation logic.`); } + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + const queryParams: Record = {}; + + const headers: Record = { 'Accept': 'application/json' }; + + // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "GET", + url: requestUrl, + params: queryParams, + headers: headers, + // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = `(Status: ${response.status} - No body content)`; + } + return { content: [ { type: "text", text: `API Response (Status: ${response.status}):\n${responseText}` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = `Error executing tool '${toolName}': ${error.message}`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(`Error during execution of tool '${toolName}':`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + } else + // Handler for tool: updateUser + if (toolName === "updateUser") { + try { + + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(`Arguments validated successfully for tool '${toolName}'.`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(`Unexpected error during argument validation setup for tool '${toolName}':`, error); + return { content: [{ type: 'text', text: `Internal server error during argument validation setup for tool '${toolName}'.` }] }; + } + } + // --- End Argument Validation --- + + // --- API Call Preparation --- + let urlPath = "/user/{username}"; + const username_val = validatedArgs['username']; + if (typeof username_val !== 'undefined' && username_val !== null) { urlPath = urlPath.replace("{username}", encodeURIComponent(String(username_val))); } + if (urlPath.includes('{')) { throw new Error(`Validation passed but failed to resolve path parameters in URL: ${urlPath}. Check schema/validation logic.`); } + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + const queryParams: Record = {}; + + const headers: Record = { 'Accept': 'application/json' }; + // Set Content-Type based on OpenAPI spec (or fallback) + if (typeof validatedArgs?.['requestBody'] !== 'undefined') { headers['content-type'] = 'application/json'; } + + let requestBodyData: any = undefined; + if (validatedArgs && typeof validatedArgs['requestBody'] !== 'undefined') { + requestBodyData = validatedArgs['requestBody']; + } + // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "PUT", + url: requestUrl, + params: queryParams, + headers: headers, + data: requestBodyData, // Pass the prepared request body data // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = `(Status: ${response.status} - No body content)`; + } + return { content: [ { type: "text", text: `API Response (Status: ${response.status}):\n${responseText}` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = `Error executing tool '${toolName}': ${error.message}`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(`Error during execution of tool '${toolName}':`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + } else + // Handler for tool: deleteUser + if (toolName === "deleteUser") { + try { + + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(`Arguments validated successfully for tool '${toolName}'.`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(`Unexpected error during argument validation setup for tool '${toolName}':`, error); + return { content: [{ type: 'text', text: `Internal server error during argument validation setup for tool '${toolName}'.` }] }; + } + } + // --- End Argument Validation --- + + // --- API Call Preparation --- + let urlPath = "/user/{username}"; + const username_val = validatedArgs['username']; + if (typeof username_val !== 'undefined' && username_val !== null) { urlPath = urlPath.replace("{username}", encodeURIComponent(String(username_val))); } + if (urlPath.includes('{')) { throw new Error(`Validation passed but failed to resolve path parameters in URL: ${urlPath}. Check schema/validation logic.`); } + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + const queryParams: Record = {}; + + const headers: Record = { 'Accept': 'application/json' }; + + // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "DELETE", + url: requestUrl, + params: queryParams, + headers: headers, + // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = `(Status: ${response.status} - No body content)`; + } + return { content: [ { type: "text", text: `API Response (Status: ${response.status}):\n${responseText}` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = `Error executing tool '${toolName}': ${error.message}`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(`Error during execution of tool '${toolName}':`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + } // This generated code now includes Zod validation + + // Fallback error + console.error(`Error: Handler logic missing for tool: ${toolName}. This indicates an issue in the generator.`); + return { content: [{ type: "text", text: `Error: Internal server error - handler not implemented for tool: ${toolName}` }] }; +}); + + +// --- Main Execution Function --- +async function main() { + try { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error(`${SERVER_NAME} MCP Server (v${SERVER_VERSION}) running on stdio${API_BASE_URL ? `, proxying API at ${API_BASE_URL}` : ''}`); + } catch (error) { + console.error("Error during server startup:", error); + process.exit(1); + } +} + +// --- Cleanup Function --- +async function cleanup() { + console.error("Shutting down MCP server..."); + process.exit(0); +} + +// Register signal handlers +process.on('SIGINT', cleanup); +process.on('SIGTERM', cleanup); + +// --- Start the Server --- +main().catch((error) => { + console.error("Fatal error in main execution:", error); + process.exit(1); +}); + +// --- Helper Functions (Included in the generated server code) --- +function formatApiError(error: AxiosError): string { + let message = 'API request failed.'; + if (error.response) { + message = `API Error: Status ${error.response.status} (${error.response.statusText || 'Status text not available'}). `; + const responseData = error.response.data; + const MAX_LEN = 200; + if (typeof responseData === 'string') { + message += `Response: ${responseData.substring(0, MAX_LEN)}${responseData.length > MAX_LEN ? '...' : ''}`; + } else if (responseData) { + try { + const jsonString = JSON.stringify(responseData); + message += `Response: ${jsonString.substring(0, MAX_LEN)}${jsonString.length > MAX_LEN ? '...' : ''}`; + } catch { + message += 'Response: [Could not serialize response data]'; + } + } else { + message += 'No response body received.'; + } + } else if (error.request) { + message = 'API Network Error: No response received from the server. Check network connectivity or server availability.'; + if (error.code) message += ` (Code: ${error.code})`; + } else { + message = `API Request Setup Error: ${error.message}`; + } + return message; +} + +/** + * Attempts to dynamically generate and evaluate a Zod schema from a JSON schema. + * WARNING: Uses eval(), which can be a security risk if the schema input is untrusted. + * In this context, the schema originates from the generator/OpenAPI spec, reducing risk. + * @param jsonSchema The JSON Schema object (or boolean). + * @param toolName For error logging. + * @returns The evaluated Zod schema object. + * @throws If schema conversion or evaluation fails. + */ +function getZodSchemaFromJsonSchema(jsonSchema: any, toolName: string): z.ZodTypeAny { + if (typeof jsonSchema !== 'object' || jsonSchema === null) { + // Handle boolean schemas or invalid input + console.warn(`Cannot generate Zod schema for non-object JSON schema for tool '${toolName}'. Input type: ${typeof jsonSchema}`) + // Fallback to allowing any object - adjust if stricter handling is needed + return z.object({}).passthrough(); + } + try { + // Note: jsonSchemaToZod may require specific configurations or adjustments + // depending on the complexity of the JSON Schemas being converted. + const zodSchemaString = jsonSchemaToZod(jsonSchema); + + // IMPORTANT: Using eval() to execute the generated Zod schema string. + // This is generally discouraged due to security risks with untrusted input. + // Ensure the JSON schemas processed here are from trusted sources (like your OpenAPI spec). + // The 'z' variable (from imported zod) must be in scope for eval. + const zodSchema = eval(zodSchemaString); + + if (typeof zodSchema?.parse !== 'function') { + throw new Error('Generated Zod schema string did not evaluate to a valid Zod schema object.'); + } + return zodSchema as z.ZodTypeAny; + } catch (err: any) { + console.error(`Failed to generate or evaluate Zod schema for tool '${toolName}':`, err); + // Fallback schema in case of conversion/evaluation error + // This allows any object, effectively skipping validation on error. + // Consider throwing the error if validation is critical. + return z.object({}).passthrough(); + } +} diff --git a/examples/petstore-mcp/tsconfig.json b/examples/petstore-mcp/tsconfig.json new file mode 100644 index 0000000..e4cecf3 --- /dev/null +++ b/examples/petstore-mcp/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": false, + "outDir": "./build", + "declaration": true, + "sourceMap": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "build" + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 32e9b5f..d494a80 100644 --- a/package.json +++ b/package.json @@ -1,38 +1,50 @@ { - "name": "openapi-mcp-generator", - "version": "1.0.4", - "description": "Generate MCP server code from OpenAPI specifications", - "type": "module", - "main": "src/index.js", - "bin": { - "openapi-mcp-generator": "./src/index.js" - }, - "scripts": { - "start": "node src/index.js", - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [ - "openapi", - "mcp", - "model-context-protocol", - "generator", - "llm" - ], - "author": "Harsha", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/harsha-iiiv/openapi-mcp-generator.git" - }, - "bugs": { - "url": "https://github.com/harsha-iiiv/openapi-mcp-generator/issues" - }, - "homepage": "https://github.com/harsha-iiiv/openapi-mcp-generator#readme", - "dependencies": { - "axios": "^1.6.0", - "minimist": "^1.2.8" - }, - "engines": { - "node": ">=16.0.0" - } + "name": "openapi-mcp-generator", + "version": "2.0.0", + "description": "Generates MCP server code from OpenAPI specifications", + "license": "MIT", + "author": "Harsha", + "type": "module", + "engines": { + "node": ">=18.0.0" + }, + "bin": { + "openapi-mcp-generator": "./build/index.js" + }, + "files": [ + "build" + ], + "scripts": { + "start": "node build/index.js", + "typecheck": "tsc --noEmit", + "build": "tsc && chmod 755 build/index.js" + }, + "keywords": [ + "openapi", + "mcp", + "model-context-protocol", + "generator", + "llm" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/harsha-iiiv/openapi-mcp-generator.git" + }, + "bugs": { + "url": "https://github.com/harsha-iiiv/openapi-mcp-generator/issues" + }, + "homepage": "https://github.com/harsha-iiiv/openapi-mcp-generator#readme", + "dependencies": { + "@apidevtools/swagger-parser": "^10.1.1", + "@modelcontextprotocol/sdk": "^1.9.0", + "axios": "^1.8.4", + "commander": "^13.1.0", + "zod": "^3.24.2" + }, + "devDependencies": { + "typescript": "^5.8.3" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.9.0" + } } diff --git a/src/config-generator.js b/src/config-generator.js deleted file mode 100644 index 991eb70..0000000 --- a/src/config-generator.js +++ /dev/null @@ -1,323 +0,0 @@ -/** - * Generate .env.example with enhanced auth examples - */ -function generateEnvExample(config, securitySchemes) { - console.log('Generating .env.example file...'); - let authExamples = ''; - if (securitySchemes && Object.keys(securitySchemes).length > 0) { - authExamples = `# --- Authorization Configuration --- \n`; - for (const [schemeName, schemeDef] of Object.entries(securitySchemes)) { - if (schemeDef.type === 'apiKey') { - authExamples += `# Example for API Key "${schemeName}" (${schemeDef.in || 'header'}: ${schemeDef.name})\n`; - authExamples += `${schemeName.toUpperCase()}_${schemeDef.name.toUpperCase()}=YOUR_API_KEY_VALUE\n`; - } else if (schemeDef.type === 'http' && schemeDef.scheme === 'bearer') { - authExamples += `# Example for HTTP Bearer Token "${schemeName}"\n`; - authExamples += `${schemeName.toUpperCase()}_BEARERTOKEN=YOUR_BEARER_TOKEN_VALUE\n`; - } else if (schemeDef.type === 'http' && schemeDef.scheme === 'basic') { - authExamples += `# Example for HTTP Basic Auth "${schemeName}"\n`; - authExamples += `${schemeName.toUpperCase()}_USERNAME=YOUR_USERNAME\n`; - authExamples += `${schemeName.toUpperCase()}_PASSWORD=YOUR_PASSWORD\n`; - } - } - } - - return `# API Configuration -API_BASE_URL=https://api.example.com -API_HEADERS= # (Less common now, use specific auth env vars below instead) - -# Server Configuration -SERVER_NAME=${config.name} -SERVER_VERSION=${config.version} -TRANSPORT=stdio # Fixed to stdio - -# Debug -DEBUG=false - -${authExamples} -`; -} - -/** - * Generate README.md with enhanced auth instructions - */ -function generateReadme(config, spec, tools, hasSecuritySchemes) { - console.log('Generating README.md file...'); - const readme = `# ${config.name} - -Model Context Protocol (MCP) server for ${spec.info?.title || 'OpenAPI'} API. - -## Description - -${spec.info?.description || 'This server provides a Model Context Protocol (MCP) interface to the API.'} - -## Installation - -1. Install dependencies: - -\`\`\`bash -npm install -\`\`\` - -2. Create a \`.env\` file based on \`.env.example\`: - -\`\`\`bash -cp .env.example .env -\`\`\` - -3. Edit the \`.env\` file to add your API configuration and authorization details. - -## Configuration - -The following environment variables can be configured in the \`.env\` file: - -- \`API_BASE_URL\`: Base URL for the API (required) -- \`SERVER_NAME\`: Name of the MCP server (default: "${config.name}") -- \`SERVER_VERSION\`: Version of the MCP server (default: "${config.version}") -- \`DEBUG\`: Enable debug logging (true/false) (default: false) - -${hasSecuritySchemes ? ` -### Authorization Configuration - -This server supports the following authorization schemes defined in the OpenAPI specification: - -${Object.entries(spec.components?.securitySchemes || {}).map(([schemeName, schemeDef]) => { - let configDetails = ''; - if (schemeDef.type === 'apiKey') { - configDetails = `- **${schemeName} (API Key)**: Set environment variable \`${schemeName.toUpperCase()}_${schemeDef.name.toUpperCase()}\` with your API key. The key will be sent in the \`${schemeDef.name}\` ${schemeDef.in || 'header'}.`; - } else if (schemeDef.type === 'http' && schemeDef.scheme === 'bearer') { - configDetails = `- **${schemeName} (HTTP Bearer)**: Set environment variable \`${schemeName.toUpperCase()}_BEARERTOKEN\` with your Bearer token. The token will be sent in the \`Authorization\` header.`; - } else if (schemeDef.type === 'http' && schemeDef.scheme === 'basic') { - configDetails = `- **${schemeName} (HTTP Basic)**: Set environment variables \`${schemeName.toUpperCase()}_USERNAME\` and \`${schemeName.toUpperCase()}_PASSWORD\` with your credentials. These will be encoded and sent in the \`Authorization\` header.`; - } else { - configDetails = `- **${schemeName} (${schemeDef.type})**: Configuration details for this scheme type are not fully described in this template. Refer to the OpenAPI specification and update \`.env.example\` and server code manually if needed.`; - } - return configDetails; - }).join('\n\n')} - -`: ''} - -## Usage - -### Running the Server - -The server is provided as both JavaScript and TypeScript versions: - -\`\`\`bash -# Run JavaScript version -npm start - -# Or run TypeScript version (compiles on the fly) -npm run start:ts -\`\`\` - -### Building the TypeScript Version - -\`\`\`bash -npm run build -cd dist -node server.js -\`\`\` - -## Using as an MCP Tool Provider - -This server implements the Model Context Protocol (MCP) and can be used with any MCP-compatible consumer, like Claude.js client or other MCP consumers. - -Example of connecting to this server from a Claude.js client: - -\`\`\`javascript -import { MCP } from "claude-js"; -import { createStdio } from "claude-js/mcp"; - -// Create stdin/stdout transport -const transport = createStdio({ command: "node path/to/server.js" }); - -// Connect to the MCP server -const mcp = new MCP({ transport }); -await mcp.connect(); - -// List available tools -const { tools } = await mcp.listTools(); -console.log("Available tools:", tools); - -// Call a tool -const result = await mcp.callTool({ - id: "TOOL-ID", - arguments: { param1: "value1" } -}); -console.log("Tool result:", result); -\`\`\` - -## Available Tools - -This MCP server provides the following tools: - -${tools.map(tool => `### ${tool.name} - -- **ID**: \`${tool.id}\` -- **Description**: ${tool.description || 'No description provided'} -- **Method**: \`${tool.method}\` -- **Path**: \`${tool.path}\` - -${Object.keys(tool.inputSchema.properties).length > 0 ? '**Parameters**:\n\n' + - Object.entries(tool.inputSchema.properties).map(([name, prop]) => - `- \`${name}\`: ${prop.description || name} ${tool.inputSchema.required?.includes(name) ? '(required)' : ''}` - ).join('\n') : 'No parameters required.'}`).join('\n\n')} - -## License - -MIT -`; - return readme; -} - -/** - * Generate package.json for the MCP server - */ -function generatePackageJson(config, spec) { - console.log('Generating package.json file...'); - const packageJson = { - name: config.name.toLowerCase().replace(/\s+/g, '-'), - version: config.version, - description: `MCP server for ${spec.info?.title || 'OpenAPI'} API`, - type: 'module', - main: 'server.js', - scripts: { - start: 'node server.js', - build: 'node build.js', - "start:ts": "npx tsc && node dist/server.js" - }, - dependencies: { - '@modelcontextprotocol/sdk': '^1.0.0', - 'axios': '^1.6.0', - 'dotenv': '^16.0.0', - }, - devDependencies: { - '@types/node': '^20.11.0', - 'typescript': '^5.3.3' - }, - engines: { - 'node': '>=16.0.0' - } - }; - - return JSON.stringify(packageJson, null, 2); -} - -/** - * Generate a TypeScript declaration file - */ -function generateTypeDefinitions(tools) { - console.log('Generating types.d.ts file...'); - return `/** - * Type definitions for the API endpoints - * Auto-generated from OpenAPI specification - */ - -export interface APITools { -${tools.map(tool => ` /** - * ${tool.description || tool.name} - */ - "${tool.id}": { - params: { -${Object.entries(tool.inputSchema.properties).map(([name, prop]) => - ` /** - * ${prop.description || name} - */ - ${name}${tool.inputSchema.required?.includes(name) ? '' : '?'}: ${prop.type === 'integer' ? 'number' : prop.type};` - ).join('\n')} - }; - response: any; // Response structure will depend on the API - };`).join('\n\n')} -} -`; -} - -/** - * Generate the tsconfig.json file - */ -function generateTsConfig() { - console.log('Generating tsconfig.json file...'); - return `{ - "compilerOptions": { - "target": "es2020", - "module": "esnext", - "moduleResolution": "node", - "esModuleInterop": true, - "strict": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "outDir": "dist" - }, - "include": ["*.ts", "*.js"], - "exclude": ["node_modules"] -} -`; -} - -/** - * Generate build.js script for TypeScript compilation - */ -function generateBuildScript() { - console.log('Generating build.js file...'); - return `#!/usr/bin/env node - -import { exec } from 'child_process'; -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -// Get proper paths for ES modules -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// Ensure dist directory exists -if (!fs.existsSync('./dist')) { - fs.mkdirSync('./dist'); -} - -// Run TypeScript compiler -console.log('Compiling TypeScript...'); -exec('npx tsc', (error, stdout, stderr) => { - if (error) { - console.error('Error compiling TypeScript:', error); - console.error(stderr); - process.exit(1); - } - - if (stdout) { - console.log(stdout); - } - - console.log('TypeScript compilation successful'); - - // Copy .env.example to dist - try { - if (fs.existsSync('./.env.example')) { - fs.copyFileSync('./.env.example', './dist/.env.example'); - console.log('Copied .env.example to dist directory'); - } - - // Create package.json in dist - const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); - packageJson.main = 'server.js'; - fs.writeFileSync('./dist/package.json', JSON.stringify(packageJson, null, 2)); - console.log('Created package.json in dist directory'); - - console.log('Build completed successfully'); - } catch (err) { - console.error('Error copying files:', err); - process.exit(1); - } -}); -`; -} - - -export { - generatePackageJson, - generateReadme, - generateEnvExample, - generateTypeDefinitions, - generateTsConfig, - generateBuildScript -}; \ No newline at end of file diff --git a/src/file-utils.js b/src/file-utils.js deleted file mode 100644 index 5df292e..0000000 --- a/src/file-utils.js +++ /dev/null @@ -1,22 +0,0 @@ -import { writeFile } from 'fs/promises'; -import path from 'path'; - -/** - * Copy the required MCP server template file if it doesn't exist in the output - */ -async function copyTemplateFile(file, content, outputDir, verbose = false) { - const outputPath = path.join(outputDir, file); - try { - await writeFile(outputPath, content); - console.log(`✓ Created ${outputPath}`); - if (verbose) { - console.log(` File size: ${content.length} bytes`); - } - return true; - } catch (error) { - console.error(`✗ Failed to create ${outputPath}: ${error.message}`); - throw error; - } -} - -export { copyTemplateFile }; \ No newline at end of file diff --git a/src/generator.ts b/src/generator.ts new file mode 100644 index 0000000..de7767e --- /dev/null +++ b/src/generator.ts @@ -0,0 +1,604 @@ +import { OpenAPIV3 } from 'openapi-types'; +import type { JSONSchema7, JSONSchema7TypeName } from 'json-schema'; +import { generateOperationId } from './utils.js'; + +interface CliOptions { + input: string; + output: string; // This is the directory path + serverName?: string; + serverVersion?: string; + baseUrl?: string; +} + +interface McpToolDefinition { + name: string; + description: string; + inputSchema: JSONSchema7 | boolean; + operationId: string; + method: string; + path: string; + parameters: OpenAPIV3.ParameterObject[]; + requestBody?: OpenAPIV3.RequestBodyObject; +} + + +/** + * Generates the TypeScript code content for the server's src/index.ts file. + */ +export function generateMcpServerCode( + api: OpenAPIV3.Document, + options: CliOptions, + serverName: string, + serverVersion: string +): string { + + const tools: McpToolDefinition[] = extractToolsFromApi(api); + const determinedBaseUrl = determineBaseUrl(api, options.baseUrl); + const listToolsCode = generateListTools(tools); + const callToolCode = generateCallTool(tools, determinedBaseUrl); + + // --- Template for src/index.ts --- + return ` +// Generated by openapi-to-mcp-generator for ${serverName} v${serverVersion} +// Source OpenAPI spec: ${options.input} +// Generation date: ${new Date().toISOString()} + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; + +// Import Schemas and Types from /types subpath with .js extension +import { + CallToolRequestSchema, + ListToolsRequestSchema, + type Tool, + type CallToolResult, + type CallToolRequest // Added type for the request parameter +} from "@modelcontextprotocol/sdk/types.js"; + +// Zod for runtime validation +import { z, ZodError } from 'zod'; +// Library to convert JSON Schema to Zod schema string at runtime +import { jsonSchemaToZod } from 'json-schema-to-zod'; + +// Define JsonObject locally as a utility type +type JsonObject = Record; + +import axios, { type AxiosRequestConfig, type AxiosError } from 'axios'; + +// --- Server Configuration --- +const SERVER_NAME = "${serverName}"; +const SERVER_VERSION = "${serverVersion}"; +const API_BASE_URL = "${determinedBaseUrl || ''}"; + +// --- Server Instance --- +const server = new Server( + { + name: SERVER_NAME, + version: SERVER_VERSION + }, + { + capabilities: { + tools: {} + } + } +); + +// --- Tool Definitions (for ListTools response) --- +// Corrected: Use Tool[] type +const toolsList: Tool[] = [ +${listToolsCode} +]; + +// --- Request Handlers --- + +// 1. List Available Tools Handler +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: toolsList, + }; +}); + +// 2. Call Tool Handler +// Corrected: Added explicit type for 'request' parameter +server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest): Promise => { + const { name: toolName, arguments: toolArgs } = request.params; + + const toolDefinition = toolsList.find(t => t.name === toolName); + + if (!toolDefinition) { + console.error(\`Error: Received request for unknown tool: \${toolName}\`); + return { content: [{ type: "text", text: \`Error: Unknown tool requested: \${toolName}\` }] }; + } + + // --- Tool Execution Logic --- +${callToolCode} // This generated code now includes Zod validation + + // Fallback error + console.error(\`Error: Handler logic missing for tool: \${toolName}. This indicates an issue in the generator.\`); + return { content: [{ type: "text", text: \`Error: Internal server error - handler not implemented for tool: \${toolName}\` }] }; +}); + + +// --- Main Execution Function --- +async function main() { + try { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error(\`\${SERVER_NAME} MCP Server (v\${SERVER_VERSION}) running on stdio\${API_BASE_URL ? \`, proxying API at \${API_BASE_URL}\` : ''}\`); + } catch (error) { + console.error("Error during server startup:", error); + process.exit(1); + } +} + +// --- Cleanup Function --- +async function cleanup() { + console.error("Shutting down MCP server..."); + process.exit(0); +} + +// Register signal handlers +process.on('SIGINT', cleanup); +process.on('SIGTERM', cleanup); + +// --- Start the Server --- +main().catch((error) => { + console.error("Fatal error in main execution:", error); + process.exit(1); +}); + +// --- Helper Functions (Included in the generated server code) --- +function formatApiError(error: AxiosError): string { + let message = 'API request failed.'; + if (error.response) { + message = \`API Error: Status \${error.response.status} (\${error.response.statusText || 'Status text not available'}). \`; + const responseData = error.response.data; + const MAX_LEN = 200; + if (typeof responseData === 'string') { + message += \`Response: \${responseData.substring(0, MAX_LEN)}\${responseData.length > MAX_LEN ? '...' : ''}\`; + } else if (responseData) { + try { + const jsonString = JSON.stringify(responseData); + message += \`Response: \${jsonString.substring(0, MAX_LEN)}\${jsonString.length > MAX_LEN ? '...' : ''}\`; + } catch { + message += 'Response: [Could not serialize response data]'; + } + } else { + message += 'No response body received.'; + } + } else if (error.request) { + message = 'API Network Error: No response received from the server. Check network connectivity or server availability.'; + if (error.code) message += \` (Code: \${error.code})\`; + } else { + message = \`API Request Setup Error: \${error.message}\`; + } + return message; +} + +/** + * Attempts to dynamically generate and evaluate a Zod schema from a JSON schema. + * WARNING: Uses eval(), which can be a security risk if the schema input is untrusted. + * In this context, the schema originates from the generator/OpenAPI spec, reducing risk. + * @param jsonSchema The JSON Schema object (or boolean). + * @param toolName For error logging. + * @returns The evaluated Zod schema object. + * @throws If schema conversion or evaluation fails. + */ +function getZodSchemaFromJsonSchema(jsonSchema: any, toolName: string): z.ZodTypeAny { + if (typeof jsonSchema !== 'object' || jsonSchema === null) { + // Handle boolean schemas or invalid input + console.warn(\`Cannot generate Zod schema for non-object JSON schema for tool '\${toolName}'. Input type: \${typeof jsonSchema}\`) + // Fallback to allowing any object - adjust if stricter handling is needed + return z.object({}).passthrough(); + } + try { + // Note: jsonSchemaToZod may require specific configurations or adjustments + // depending on the complexity of the JSON Schemas being converted. + const zodSchemaString = jsonSchemaToZod(jsonSchema); + + // IMPORTANT: Using eval() to execute the generated Zod schema string. + // This is generally discouraged due to security risks with untrusted input. + // Ensure the JSON schemas processed here are from trusted sources (like your OpenAPI spec). + // The 'z' variable (from imported zod) must be in scope for eval. + const zodSchema = eval(zodSchemaString); + + if (typeof zodSchema?.parse !== 'function') { + throw new Error('Generated Zod schema string did not evaluate to a valid Zod schema object.'); + } + return zodSchema as z.ZodTypeAny; + } catch (err: any) { + console.error(\`Failed to generate or evaluate Zod schema for tool '\${toolName}':\`, err); + // Fallback schema in case of conversion/evaluation error + // This allows any object, effectively skipping validation on error. + // Consider throwing the error if validation is critical. + return z.object({}).passthrough(); + } +} +`; +} + +/** + * Generates the content for the package.json file for a buildable project. + * Adds zod and json-schema-to-zod dependencies. + */ +export function generatePackageJson(serverName: string, serverVersion: string): string { + const packageData = { + name: serverName, + version: serverVersion, + description: `MCP Server generated from OpenAPI spec for ${serverName}`, + private: true, + type: "module", + main: "build/index.js", + files: [ "build", "src" ], + scripts: { + "start": "node build/index.js", + "build": "tsc && chmod 755 build/index.js", + "typecheck": "tsc --noEmit", + "prestart": "npm run build" + }, + engines: { + "node": ">=18.0.0" + }, + dependencies: { + "@modelcontextprotocol/sdk": "^1.9.0", + "axios": "^1.8.4", + "zod": "^3.24.2", + "json-schema-to-zod": "^2.6.1" + }, + devDependencies: { + "@types/node": "^18.19.0", + "typescript": "^5.4.5" + // Removed ts-node, tsc-watch + } + }; + return JSON.stringify(packageData, null, 2); +} + +/** + * Generates the content for the tsconfig.json file for a buildable project. + * Enables stricter type checking. + */ +export function generateTsconfigJson(): string { + const tsconfigData = { + compilerOptions: { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] + }; + return JSON.stringify(tsconfigData, null, 2); +} + +/** + * Generates the content for the .gitignore file. + */ +export function generateGitignore(): string { + // Content unchanged from previous version + return ` +# Node dependencies +node_modules +# Build output +dist +build + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory +coverage +*.lcov +.nyc_output + +# Build artifacts +.grunt +bower_components +# build/Release # Covered by build/ above +jspm_packages/ +web_modules/ +.lock-wscript + +# VS Code files +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Caches +.eslintcache +.node_repl_history +.browserslistcache + +# Environment variables +.env +.env.*.local +.env.local +`; +} + + +// --- Helper Functions below are mostly unchanged, except generateCallTool --- + +function determineBaseUrl(api: OpenAPIV3.Document, cmdLineBaseUrl?: string): string | null { + // Logic unchanged + if (cmdLineBaseUrl) return cmdLineBaseUrl.replace(/\/$/, ''); + if (api.servers && api.servers.length === 1 && api.servers[0].url) return api.servers[0].url.replace(/\/$/, ''); + if (api.servers && api.servers.length > 1) { + console.warn(`⚠️ Multiple servers found. Using first: "${api.servers[0].url}". Use --base-url to override.`); + return api.servers[0].url.replace(/\/$/, ''); + } + return null; +} + +function extractToolsFromApi(api: OpenAPIV3.Document): McpToolDefinition[] { + // Logic unchanged + const tools: McpToolDefinition[] = []; + const usedNames = new Set(); + if (!api.paths) return tools; + for (const [path, pathItem] of Object.entries(api.paths)) { + if (!pathItem) continue; + for (const method of Object.values(OpenAPIV3.HttpMethods)) { + const operation = pathItem[method]; + if (!operation) continue; + let baseName = operation.operationId || generateOperationId(method, path); + if (!baseName) { + console.warn(`⚠️ Skipping ${method.toUpperCase()} ${path}: missing operationId.`); + continue; + } + let finalToolName = baseName; + let counter = 1; + while (usedNames.has(finalToolName)) finalToolName = `${baseName}_${counter++}`; + usedNames.add(finalToolName); + const description = operation.description || operation.summary || `Executes ${method.toUpperCase()} ${path}`; + const { inputSchema, parameters, requestBody } = generateInputSchema(operation); + tools.push({ name: finalToolName, description, inputSchema, operationId: baseName, method, path, parameters, requestBody }); + } + } + return tools; +} + +function generateInputSchema(operation: OpenAPIV3.OperationObject): { inputSchema: JSONSchema7 | boolean, parameters: OpenAPIV3.ParameterObject[], requestBody?: OpenAPIV3.RequestBodyObject } { + // Logic unchanged + const properties: { [key: string]: JSONSchema7 | boolean } = {}; + const required: string[] = []; + const allParameters: OpenAPIV3.ParameterObject[] = Array.isArray(operation.parameters) + ? operation.parameters.map(p => p as OpenAPIV3.ParameterObject) : []; + allParameters.forEach(param => { + if (!param.name || !param.schema) return; + const paramSchema = mapOpenApiSchemaToJsonSchema(param.schema as OpenAPIV3.SchemaObject); + if (typeof paramSchema === 'object') paramSchema.description = param.description || paramSchema.description; + properties[param.name] = paramSchema; + if (param.required) required.push(param.name); + }); + let opRequestBody: OpenAPIV3.RequestBodyObject | undefined = undefined; + if (operation.requestBody) { + opRequestBody = operation.requestBody as OpenAPIV3.RequestBodyObject; + const jsonContent = opRequestBody.content?.['application/json']; + if (jsonContent?.schema) { + const bodySchema = mapOpenApiSchemaToJsonSchema(jsonContent.schema as OpenAPIV3.SchemaObject); + if (typeof bodySchema === 'object') bodySchema.description = opRequestBody.description || bodySchema.description || 'The JSON request body.'; + properties['requestBody'] = bodySchema; + if (opRequestBody.required) required.push('requestBody'); + } else { + const firstContent = opRequestBody.content ? Object.entries(opRequestBody.content)[0] : undefined; + if(firstContent) { + const [contentType] = firstContent; + properties['requestBody'] = { type: 'string', description: opRequestBody.description || `Request body (content type: ${contentType})` }; + if (opRequestBody.required) required.push('requestBody'); + } + } + } + const inputSchema: JSONSchema7 = { type: 'object', properties, ...(required.length > 0 && { required }) }; + return { inputSchema, parameters: allParameters, requestBody: opRequestBody }; +} + +function mapOpenApiSchemaToJsonSchema(schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject): JSONSchema7 | boolean { + // Logic mostly unchanged, ensure it handles recursion correctly + if ('$ref' in schema) { + console.warn(`⚠️ Unresolved $ref '${schema.$ref}'. Schema may be incomplete.`); + return { type: 'object', description: `Unresolved: ${schema.$ref}` }; + } + if (typeof schema === 'boolean') return schema; + const jsonSchema: JSONSchema7 = { ...schema } as any; + if (schema.type === 'integer') jsonSchema.type = 'number'; + delete (jsonSchema as any).nullable; delete (jsonSchema as any).example; delete (jsonSchema as any).xml; + delete (jsonSchema as any).externalDocs; delete (jsonSchema as any).deprecated; delete (jsonSchema as any).readOnly; delete (jsonSchema as any).writeOnly; + if (schema.nullable) { + if (Array.isArray(jsonSchema.type)) { if (!jsonSchema.type.includes('null')) jsonSchema.type.push('null'); } + else if (typeof jsonSchema.type === 'string') jsonSchema.type = [jsonSchema.type as JSONSchema7TypeName, 'null']; + else if (!jsonSchema.type) jsonSchema.type = 'null'; + } + if (jsonSchema.type === 'object' && jsonSchema.properties) { + const mappedProps: { [key: string]: JSONSchema7 | boolean } = {}; + for (const [key, propSchema] of Object.entries(jsonSchema.properties)) { + if (typeof propSchema === 'object' && propSchema !== null) mappedProps[key] = mapOpenApiSchemaToJsonSchema(propSchema as OpenAPIV3.SchemaObject); + else if (typeof propSchema === 'boolean') mappedProps[key] = propSchema; + } + jsonSchema.properties = mappedProps; + } + if (jsonSchema.type === 'array' && typeof jsonSchema.items === 'object' && jsonSchema.items !== null) { + jsonSchema.items = mapOpenApiSchemaToJsonSchema(jsonSchema.items as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject); + } + return jsonSchema; +} + +function generateListTools(tools: McpToolDefinition[]): string { + // Logic unchanged + if (tools.length === 0) return " // No tools extracted from the OpenAPI spec."; + return tools.map(tool => { + const escapedDescription = (tool.description || '').replace(/\\/g, '\\\\').replace(/`/g, '\\`'); + let schemaString; try { schemaString = JSON.stringify(tool.inputSchema, null, 4).replace(/^/gm, ' '); } + catch (e) { schemaString = ' { "type": "object", "description": "Error: Could not stringify schema" }'; } + return `\n // Tool: ${tool.name} (${tool.method.toUpperCase()} ${tool.path})\n {\n name: "${tool.name}",\n description: \`${escapedDescription}\`,\n inputSchema: ${schemaString}\n },`; + }).join(''); // Changed join to empty string to avoid extra newline +} + +/** + * Generates the 'if/else if' block for the CallTool handler. + * Includes runtime Zod validation. + */ +function generateCallTool(tools: McpToolDefinition[], baseUrl: string | null): string { + if (tools.length === 0) return ' // No tools defined, so no handlers generated.'; + + const cases = tools.map(tool => { + const { name, method, path: rawPath, parameters, requestBody } = tool; + const pathParams = parameters.filter(p => p.in === 'path'); + const queryParams = parameters.filter(p => p.in === 'query'); + const headerParams = parameters.filter(p => p.in === 'header'); + + // --- Code Generation Snippets --- + // Zod validation block (remains the same) + const argsValidationCode = ` + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(\`Arguments validated successfully for tool '\${toolName}'.\`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = \`Invalid arguments for tool '\${toolName}': \${error.errors.map(e => \`\${e.path.join('.')} (\${e.code}): \${e.message}\`).join(', ')}\`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(\`Unexpected error during argument validation setup for tool '\${toolName}':\`, error); + return { content: [{ type: 'text', text: \`Internal server error during argument validation setup for tool '\${toolName}'.\` }] }; + } + } + // --- End Argument Validation --- +`; + + // URL Path Construction (uses validatedArgs) + let urlPathCode = ` let urlPath = "${rawPath}";\n`; + pathParams.forEach(p => { + urlPathCode += ` const ${p.name}_val = validatedArgs['${p.name}'];\n`; // Use distinct name to avoid clash + urlPathCode += ` if (typeof ${p.name}_val !== 'undefined' && ${p.name}_val !== null) { urlPath = urlPath.replace("{${p.name}}", encodeURIComponent(String(${p.name}_val))); }\n`; + }); + urlPathCode += ` if (urlPath.includes('{')) { throw new Error(\`Validation passed but failed to resolve path parameters in URL: \${urlPath}. Check schema/validation logic.\`); }\n`; + urlPathCode += ` const requestUrl = API_BASE_URL ? \`\${API_BASE_URL}\${urlPath}\` : urlPath;`; + + // Query Parameters Construction (uses validatedArgs) + let queryParamsCode = ' const queryParams: Record = {};\n'; + queryParams.forEach(p => { + queryParamsCode += ` const ${p.name}_val = validatedArgs['${p.name}'];\n`; // Use distinct name + queryParamsCode += ` if (typeof ${p.name}_val !== 'undefined' && ${p.name}_val !== null) queryParams['${p.name}'] = ${p.name}_val;\n`; + }); + + // Headers Construction (uses validatedArgs) + let headersCode = ` const headers: Record = { 'Accept': 'application/json' };\n`; + headerParams.forEach(p => { + headersCode += ` const ${p.name}_val = validatedArgs['${p.name}'];\n`; // Use distinct name + headersCode += ` if (typeof ${p.name}_val !== 'undefined' && ${p.name}_val !== null) headers['${p.name.toLowerCase()}'] = String(${p.name}_val);\n`; + }); + + // **Corrected Request Body Handling** + let requestBodyDeclarationCode = ''; // Code to declare and assign requestBodyData + let axiosDataProperty = ''; // String part for the Axios config's 'data' property + let requestContentType = 'application/json'; // Default assumption + + if (requestBody) { // Only generate body handling if the tool expects one + // Declare the variable *before* config construction + requestBodyDeclarationCode = ` let requestBodyData: any = undefined;\n`; + // Assign value *after* validation (which sets validatedArgs) + requestBodyDeclarationCode += ` if (validatedArgs && typeof validatedArgs['requestBody'] !== 'undefined') {\n`; + requestBodyDeclarationCode += ` requestBodyData = validatedArgs['requestBody'];\n`; + requestBodyDeclarationCode += ` }\n`; + + // Determine Content-Type (must happen before headers are finalized in config) + if (requestBody.content?.['application/json']) { + requestContentType = 'application/json'; + } else if (requestBody.content) { + const firstType = Object.keys(requestBody.content)[0]; + if (firstType) { requestContentType = firstType; } + } + // Add Content-Type header *if* data might exist + headersCode += ` // Set Content-Type based on OpenAPI spec (or fallback)\n`; + headersCode += ` if (typeof validatedArgs?.['requestBody'] !== 'undefined') { headers['content-type'] = '${requestContentType}'; }\n`; + + // Set the string for the Axios config 'data' property + axiosDataProperty = 'data: requestBodyData, // Pass the prepared request body data'; + } + + // --- Assemble the 'if' block for this tool --- + // Ensure correct order: Validation -> Declarations -> Config -> Axios Call + return ` + // Handler for tool: ${name} + if (toolName === "${name}") { + try { +${argsValidationCode} + // --- API Call Preparation --- +${urlPathCode} +${queryParamsCode} +${headersCode} +${requestBodyDeclarationCode} // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "${method.toUpperCase()}", + url: requestUrl, + params: queryParams, + headers: headers, + ${axiosDataProperty} // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(\`Executing tool "\${toolName}": \${config.method} \${config.url}\`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = \`(Status: \${response.status} - No body content)\`; + } + return { content: [ { type: "text", text: \`API Response (Status: \${response.status}):\\n\${responseText}\` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = \`Error executing tool '\${toolName}': \${error.message}\`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(\`Error during execution of tool '\${toolName}':\`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + }`; + }).join(' else '); + + return cases || ' // No tools defined, so no handlers generated.'; +} \ No newline at end of file diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 15b6e23..0000000 --- a/src/index.js +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env node - -// Basic imports using ES module syntax -import minimist from 'minimist'; -import fs from 'fs'; -import { mkdir } from 'fs/promises'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -import { loadOpenAPISpec } from './openapi-loader.js'; -import { generateTools } from './tool-generator.js'; -import { generateServerFile, generateServerTS } from './server-generator.js'; -import { - generatePackageJson, - generateReadme, - generateEnvExample, - generateTypeDefinitions, - generateTsConfig, - generateBuildScript -} from './config-generator.js'; -import { copyTemplateFile } from './file-utils.js'; - -// Get proper paths for ES modules -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -console.log('Script started'); - -/** - * Main function to drive the entire process - */ -async function main() { - try { - console.log('🚀 OpenAPI to MCP Server Generator'); - - // Parse command-line arguments with minimist - const argv = minimist(process.argv.slice(2), { - string: ['openapi', 'output', 'name', 'version', 'transport'], - number: ['port'], - alias: { - o: 'openapi', - d: 'output', - n: 'name', - v: 'version', - t: 'transport', - p: 'port', - V: 'verbose' - }, - default: { - output: './mcp-server', - name: 'openapi-mcp-server', - version: '1.0.0', - transport: 'stdio', - port: 3000, - verbose: false - } - }); - - // Check required parameters - if (!argv.openapi) { - console.error('Error: --openapi parameter is required'); - console.error('Usage: ./index.js --openapi [--output ]'); - process.exit(1); - } - - // Create configuration object - const config = { - openApiSpec: argv.openapi, - outputDir: argv.output, - name: argv.name, - version: argv.version, - transport: argv.transport, - port: argv.port, - verbose: argv.verbose - }; - - console.log(`Configuration:`); - console.log(`- OpenAPI Spec: ${config.openApiSpec}`); - console.log(`- Output Directory: ${config.outputDir}`); - console.log(`- Server Name: ${config.name}`); - console.log(`- Transport: ${config.transport}`); - - // Load OpenAPI spec - const spec = await loadOpenAPISpec(config.openApiSpec, config.verbose); - - if (!spec) { - throw new Error("Failed to load or parse the OpenAPI specification"); - } - - // Check if it's a valid OpenAPI spec - if (!spec.openapi && !spec.swagger) { - console.warn("Warning: The loaded specification might not be a valid OpenAPI document. Missing 'openapi' or 'swagger' version field."); - } - - // Generate tools from spec - const { tools, toolMap, securitySchemes } = generateTools(spec, config.verbose); - const hasSecuritySchemes = Object.keys(securitySchemes).length > 0; - - - if (tools.length === 0) { - console.warn("Warning: No API tools were generated from the specification. The spec might not contain valid paths/operations."); - } - - // Create output directory if it doesn't exist - if (!fs.existsSync(config.outputDir)) { - console.log(`Creating output directory: ${config.outputDir}`); - await mkdir(config.outputDir, { recursive: true }); - } - - // Generate all the files - console.log("Generating server files..."); - const serverCode = generateServerFile(config, spec, toolMap, securitySchemes); - const serverTSCode = generateServerTS(config, spec, toolMap, securitySchemes); - const packageJson = generatePackageJson(config, spec); - const readme = generateReadme(config, spec, tools, hasSecuritySchemes); - const envExample = generateEnvExample(config, securitySchemes); - const typeDefinitions = generateTypeDefinitions(tools); - const tsConfig = generateTsConfig(); - const buildScript = generateBuildScript(); - - // Write all files - console.log("Writing files to output directory..."); - const results = await Promise.all([ - copyTemplateFile('server.js', serverCode, config.outputDir, config.verbose), - copyTemplateFile('server.ts', serverTSCode, config.outputDir, config.verbose), - copyTemplateFile('package.json', packageJson, config.outputDir, config.verbose), - copyTemplateFile('README.md', readme, config.outputDir, config.verbose), - copyTemplateFile('.env.example', envExample, config.outputDir, config.verbose), - copyTemplateFile('types.d.ts', typeDefinitions, config.outputDir, config.verbose), - copyTemplateFile('tsconfig.json', tsConfig, config.outputDir, config.verbose), - copyTemplateFile('build.js', buildScript, config.outputDir, config.verbose) - ]); - - const success = results.every(Boolean); - - if (success) { - console.log(`\n✅ MCP server generated successfully in "${config.outputDir}"`); - console.log(`📚 Generated ${tools.length} tools from OpenAPI spec`); - console.log('\nNext steps:'); - console.log('1. cd ' + config.outputDir); - console.log('2. npm install'); - console.log('3. cp .env.example .env (and edit with your API details)'); - console.log('4. Run the server:'); - console.log(' - JavaScript version: npm start'); - console.log(' - TypeScript version: npm run start:ts'); - } else { - console.error("❌ Some files failed to generate. Check the errors above."); - } - - return success; - } catch (error) { - console.error('❌ Error generating MCP server:', error.message); - if (error.stack) { - console.error(error.stack); - } - return false; - } -} - -// Run the program -main().then(success => { - process.exit(success ? 0 : 1); -}).catch(error => { - console.error('Unhandled error:', error); - process.exit(1); -}); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..b253ca9 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,112 @@ +#!/usr/bin/env node + +import { Command } from 'commander'; +import SwaggerParser from '@apidevtools/swagger-parser'; +import type { OpenAPIV3 } from 'openapi-types'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { + generateMcpServerCode, + generatePackageJson, + generateTsconfigJson, + generateGitignore +} from './generator.js'; + +interface CliOptions { + input: string; + output: string; + serverName?: string; + serverVersion?: string; + baseUrl?: string; +} + +const program = new Command(); + +program + .name('openapi-mcp-generator') + .description('Generates a buildable MCP server project (TypeScript) from an OpenAPI specification') + // Ensure these option definitions are robust + .requiredOption('-i, --input ', 'Path or URL to the OpenAPI specification file (JSON or YAML)') + .requiredOption('-o, --output ', 'Path to the directory where the MCP server project will be created (e.g., ./petstore-mcp)') + .option('-n, --server-name ', 'Name for the generated MCP server package (default: derived from OpenAPI info title)') + .option('-v, --server-version ', 'Version for the generated MCP server (default: derived from OpenAPI info version or 0.1.0)') + .option('-b, --base-url ', 'Base URL for the target API. Required if not specified in OpenAPI `servers` or if multiple servers exist.') + .version('2.0.0'); // Match package.json version + +// Parse arguments explicitly from process.argv +// This is generally the most reliable way +program.parse(process.argv); + +// Retrieve the options AFTER parsing +const options = program.opts(); + +async function main() { + // Use the parsed options directly + const outputDir = options.output; + const inputSpec = options.input; // Use the parsed input value + + const srcDir = path.join(outputDir, 'src'); + const serverFilePath = path.join(srcDir, 'index.ts'); + const packageJsonPath = path.join(outputDir, 'package.json'); + const tsconfigPath = path.join(outputDir, 'tsconfig.json'); + const gitignorePath = path.join(outputDir, '.gitignore'); + + try { + // Use the correct inputSpec variable + console.error(`Parsing OpenAPI spec: ${inputSpec}`); + const api = await SwaggerParser.dereference(inputSpec) as OpenAPIV3.Document; + console.error('OpenAPI spec parsed successfully.'); + + // Use options directly for name/version/baseUrl determination + const serverNameRaw = options.serverName || api.info.title || 'my-mcp-server'; + const serverName = serverNameRaw.toLowerCase().replace(/[^a-z0-9_-]/g, '-'); + const serverVersion = options.serverVersion || api.info.version || '0.1.0'; + + console.error('Generating server code...'); + // Pass inputSpec to generator function if needed for comments, otherwise just options + const serverTsContent = generateMcpServerCode(api, options, serverName, serverVersion); + + console.error('Generating package.json...'); + const packageJsonContent = generatePackageJson(serverName, serverVersion); + + console.error('Generating tsconfig.json...'); + const tsconfigJsonContent = generateTsconfigJson(); + + console.error('Generating .gitignore...'); + const gitignoreContent = generateGitignore(); + + console.error(`Creating project directory structure at: ${outputDir}`); + await fs.mkdir(srcDir, { recursive: true }); + + await fs.writeFile(serverFilePath, serverTsContent); + console.error(` -> Created ${serverFilePath}`); + await fs.writeFile(packageJsonPath, packageJsonContent); + console.error(` -> Created ${packageJsonPath}`); + await fs.writeFile(tsconfigPath, tsconfigJsonContent); + console.error(` -> Created ${tsconfigPath}`); + await fs.writeFile(gitignorePath, gitignoreContent); + console.error(` -> Created ${gitignorePath}`); + + console.error("\n---"); + console.error(`MCP server project '${serverName}' successfully generated at: ${outputDir}`); + console.error("\nNext steps:"); + console.error(`1. Navigate to the directory: cd ${outputDir}`); + console.error(`2. Install dependencies: npm install`); + console.error(`3. Build the TypeScript code: npm run build`); + console.error(`4. Run the server: npm start`); + console.error(" (This runs the built JavaScript code in build/index.js)"); + console.error("---"); + + } catch (error) { + console.error('\nError generating MCP server project:', error); + try { + await fs.rm(outputDir, { recursive: true, force: true }); + console.error(`Cleaned up partially created directory: ${outputDir}`); + } catch (cleanupError) { + console.error(`Failed to cleanup directory ${outputDir}:`, cleanupError); + } + process.exit(1); + } +} + +main(); \ No newline at end of file diff --git a/src/openapi-loader.js b/src/openapi-loader.js deleted file mode 100644 index 8768915..0000000 --- a/src/openapi-loader.js +++ /dev/null @@ -1,49 +0,0 @@ -import fs from 'fs'; -import { readFile } from 'fs/promises'; -import path from 'path'; -import axios from 'axios'; - -/** - * Load OpenAPI specification from file or URL - */ -async function loadOpenAPISpec(specPath, verbose = false) { - try { - if (specPath.startsWith('http')) { - // Load from URL - console.log(`Loading OpenAPI spec from URL: ${specPath}`); - const response = await axios.get(specPath); - if (verbose) { - console.log(`Successfully loaded OpenAPI spec from URL (${Object.keys(response.data).length} keys in spec)`); - } - return response.data; - } else { - // Load from local file - const resolvedPath = path.resolve(specPath); - console.log(`Loading OpenAPI spec from file: ${resolvedPath}`); - - if (!fs.existsSync(resolvedPath)) { - throw new Error(`File not found: ${resolvedPath}`); - } - - const content = await readFile(resolvedPath, 'utf-8'); - try { - const parsed = JSON.parse(content); - if (verbose) { - console.log(`Successfully loaded OpenAPI spec from file (${Object.keys(parsed).length} keys in spec)`); - } - return parsed; - } catch (parseError) { - throw new Error(`Failed to parse JSON from ${resolvedPath}: ${parseError.message}`); - } - } - } catch (error) { - console.error(`Failed to load OpenAPI spec: ${error.message}`); - if (error.response) { - console.error(`HTTP Status: ${error.response.status}`); - console.error(`Response: ${JSON.stringify(error.response.data)}`); - } - throw error; - } -} - -export { loadOpenAPISpec }; \ No newline at end of file diff --git a/src/server-generator.js b/src/server-generator.js deleted file mode 100644 index d48072d..0000000 --- a/src/server-generator.js +++ /dev/null @@ -1,766 +0,0 @@ -/** - * Generate the main server.js file with fixes for MCP compatibility - */ -function generateServerFile(config, spec, toolMap, securitySchemes) { - console.log('Generating server.js file...'); - const toolsArray = Object.values(toolMap); - const hasSecuritySchemes = Object.keys(securitySchemes).length > 0; - - // Create JavaScript version with fixes for MCP compatibility - const serverCode = `#!/usr/bin/env node - -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import axios from "axios"; -import { config as dotenvConfig } from "dotenv"; -import { - ListToolsRequestSchema, - CallToolRequestSchema, -} from "@modelcontextprotocol/sdk/types.js"; - -// Load environment variables -dotenvConfig(); - -// Define tool schemas -const TOOLS = ${JSON.stringify(toolsArray, null, 2)}; -const SECURITY_SCHEMES = ${JSON.stringify(securitySchemes, null, 2)}; - -/** - * MCP Server for ${spec.info?.title || 'OpenAPI'} API - * Generated from OpenAPI spec version ${spec.info?.version || 'unknown'} - * Generated on ${new Date().toISOString()} - */ -class MCPServer { - constructor() { - // Initialize class properties - this.server = null; - this.tools = new Map(); - this.debug = process.env.DEBUG === "true"; - this.baseUrl = process.env.API_BASE_URL || ""; - this.headers = this.parseHeaders(process.env.API_HEADERS || ""); - - // Initialize tools map - do this before creating server - this.initializeTools(); - - // Create MCP server with correct capabilities - this.server = new Server( - { - name: process.env.SERVER_NAME || "${config.name}", - version: process.env.SERVER_VERSION || "${config.version}", - }, - { - capabilities: { - tools: true, // Enable tools capability - }, - } - ); - - // Set up request handlers - don't log here - this.setupHandlers(); - } - - /** - * Parse headers from string - */ - parseHeaders(headerStr) { - const headers = {}; - if (headerStr) { - headerStr.split(",").forEach((header) => { - const [key, value] = header.split(":"); - if (key && value) headers[key.trim()] = value.trim(); - }); - } - return headers; - } - - /** - * Initialize tools map from OpenAPI spec - * This runs before the server is connected, so don't log here - */ - initializeTools() { - // Initialize each tool in the tools map - for (const tool of TOOLS) { - this.tools.set(tool.id, { - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - // Don't include security at the tool level - }); - } - - // Don't log here, we're not connected yet - console.error(\`Initialized \${this.tools.size} tools\`); - } - - /** - * Set up request handlers - */ - setupHandlers() { - // Handle tool listing requests - this.server.setRequestHandler(ListToolsRequestSchema, async () => { - this.log('debug', "Handling ListTools request"); - // Return tools in the format expected by MCP SDK - return { - tools: Array.from(this.tools.entries()).map(([id, tool]) => ({ - id, - ...tool, - })), - }; - }); - - // Handle tool execution requests - this.server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { id, name, arguments: params } = request.params; - this.log('debug', "Handling CallTool request", { id, name, params }); - - let toolId; - let toolDetails; - - try { - // Find the requested tool - toolId = id; - if (!toolId && name) { - for (const [tid, tool] of this.tools.entries()) { - if (tool.name === name) { - toolId = tid; - break; - } - } - } - - if (!toolId) { - throw new Error(\`Tool not found: \${id || name}\`); - } - - toolDetails = TOOLS.find(t => t.id === toolId); - if (!toolDetails) { - throw new Error(\`Tool details not found for ID: \${toolId}\`); - } - - this.log('info', \`Executing tool: \${toolId}\`); - - // Execute the API call - const result = await this.executeApiCall(toolDetails, params || {}); - - // Return the result in the correct MCP format - return { - content: [ - { - type: "application/json", - data: result - } - ] - }; - - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - this.log('error', \`Error executing tool \${toolId || name}: \${errorMessage}\`); - - // Format error according to MCP SDK expectations - return { - error: { - message: errorMessage, - details: error.response?.data - ? JSON.stringify(error.response.data) - : undefined - } - }; - } - }); - } - - /** - * Execute an API call for a tool - */ - async executeApiCall(tool, params) { - // Get method and path from tool - const method = tool.method; - let path = tool.path; - - // Clone params to avoid modifying the original - const requestParams = { ...params }; - - // Replace path parameters with values from params - Object.entries(requestParams).forEach(([key, value]) => { - const placeholder = \`{\${key}}\`; - if (path.includes(placeholder)) { - path = path.replace(placeholder, encodeURIComponent(String(value))); - delete requestParams[key]; // Remove used parameter - } - }); - - // Build the full URL - const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : \`\${this.baseUrl}/\`; - const cleanPath = path.startsWith("/") ? path.slice(1) : path; - const url = new URL(cleanPath, baseUrl).toString(); - - this.log('debug', \`API Request: \${method} \${url}\`); - - try { - // Configure the request - const config = { - method: method.toLowerCase(), - url, - headers: { ...this.headers }, - }; - - // Apply security headers based on tool security requirements - if (tool.security && Array.isArray(tool.security)) { - for (const requirement of tool.security) { - for (const securitySchemeName of Object.keys(requirement)) { - const securityDefinition = SECURITY_SCHEMES[securitySchemeName]; - - if (securityDefinition) { - const authType = securityDefinition.type; - - // Handle API key - if (authType === 'apiKey') { - const apiKeyName = securityDefinition.name; - const envVarName = \`\${securitySchemeName.toUpperCase()}_\${apiKeyName.toUpperCase()}\`; - const apiKeyValue = process.env[envVarName]; - - if (apiKeyValue) { - if (securityDefinition.in === 'header') { - config.headers[apiKeyName] = apiKeyValue; - } else if (securityDefinition.in === 'query') { - config.params = config.params || {}; - config.params[apiKeyName] = apiKeyValue; - } - } else { - this.log('warning', \`API Key environment variable not found: \${envVarName}\`); - } - } - // Handle bearer token - else if (authType === 'http' && securityDefinition.scheme === 'bearer') { - const envVarName = \`\${securitySchemeName.toUpperCase()}_BEARERTOKEN\`; - const bearerToken = process.env[envVarName]; - - if (bearerToken) { - config.headers['Authorization'] = \`Bearer \${bearerToken}\`; - } else { - this.log('warning', \`Bearer Token environment variable not found: \${envVarName}\`); - } - } - // Handle basic auth - else if (authType === 'http' && securityDefinition.scheme === 'basic') { - const username = process.env[\`\${securitySchemeName.toUpperCase()}_USERNAME\`]; - const password = process.env[\`\${securitySchemeName.toUpperCase()}_PASSWORD\`]; - - if (username && password) { - const auth = Buffer.from(\`\${username}:\${password}\`).toString('base64'); - config.headers['Authorization'] = \`Basic \${auth}\`; - } else { - this.log('warning', \`Basic auth credentials not found for \${securitySchemeName}\`); - } - } - } - } - } - } - - // Add parameters based on request method - if (["GET", "DELETE"].includes(method)) { - // For GET/DELETE, send params as query string - config.params = { ...(config.params || {}), ...requestParams }; - } else { - // For POST/PUT/PATCH, send params as JSON body - config.data = requestParams; - config.headers["Content-Type"] = "application/json"; - } - - this.log('debug', "Request config:", { - url: config.url, - method: config.method, - params: config.params, - headers: Object.keys(config.headers) - }); - - // Execute the request - const response = await axios(config); - this.log('debug', \`Response status: \${response.status}\`); - - return response.data; - - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - this.log('error', \`API request failed: \${errorMessage}\`); - - if (axios.isAxiosError(error)) { - const responseData = error.response?.data; - const responseStatus = error.response?.status; - - this.log('error', 'API Error Details:', { - status: responseStatus, - data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData - }); - - // Rethrow with more context for better error handling - const detailedError = new Error(\`API request failed with status \${responseStatus}: \${errorMessage}\`); - detailedError.response = error.response; - throw detailedError; - } - - throw error; - } - } - - /** - * Log messages with appropriate level - * Only sends to MCP if we're connected - */ - log(level, message, data) { - // Always log to stderr for visibility - console.error(\`[\${level.toUpperCase()}] \${message}\${data ? ': ' + JSON.stringify(data) : ''}\`); - - // Only try to send via MCP if we're in debug mode or it's important - if (this.debug || level !== 'debug') { - try { - // Only send if server exists and is connected - if (this.server && this.server.isConnected) { - this.server.sendLoggingMessage({ - level, - data: \`[MCP Server] \${message}\${data ? ': ' + JSON.stringify(data) : ''}\` - }); - } - } catch (e) { - // If logging fails, log to stderr - console.error('Failed to send log via MCP:', e.message); - } - } - } - - /** - * Start the server - */ - async start() { - try { - // Create stdio transport - const transport = new StdioServerTransport(); - console.error("MCP Server starting on stdio transport"); - - // Connect to the transport - await this.server.connect(transport); - - // Now we can safely log via MCP - console.error(\`Registered \${this.tools.size} tools\`); - this.log('info', \`MCP Server started successfully with \${this.tools.size} tools\`); - } catch (error) { - console.error("Failed to start MCP server:", error); - process.exit(1); - } - } -} - -// Start the server -async function main() { - try { - const server = new MCPServer(); - await server.start(); - } catch (error) { - console.error("Failed to start server:", error); - process.exit(1); - } -} - -main(); -`; - - return serverCode; -} - -/** - * Generate server.ts for TypeScript support with MCP compatibility fixes - */ -function generateServerTS(config, spec, toolMap, securitySchemes) { - console.log('Generating server.ts file...'); - const toolsArray = Object.values(toolMap); - const hasSecuritySchemes = Object.keys(securitySchemes).length > 0; - - const serverCode = `#!/usr/bin/env node - -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import axios, { AxiosRequestConfig, AxiosError } from "axios"; -import { config as dotenvConfig } from "dotenv"; -import { - ListToolsRequestSchema, - CallToolRequestSchema, - Tool, - JsonSchema, -} from "@modelcontextprotocol/sdk/types.js"; - -// Load environment variables -dotenvConfig(); - -// Define tool and security scheme types -interface OpenApiTool extends Tool { - method: string; - path: string; - security: any[]; -} - -interface SecurityScheme { - type: string; - name?: string; - in?: string; - scheme?: string; -} - -// Define tool schemas -const TOOLS: OpenApiTool[] = ${JSON.stringify(toolsArray, null, 2)}; -const SECURITY_SCHEMES: Record = ${JSON.stringify(securitySchemes, null, 2)}; - -/** - * MCP Server for ${spec.info?.title || 'OpenAPI'} API - * Generated from OpenAPI spec version ${spec.info?.version || 'unknown'} - * Generated on ${new Date().toISOString()} - */ -class MCPServer { - private server: Server; - private tools: Map = new Map(); - private debug: boolean; - private baseUrl: string; - private headers: Record; - - constructor() { - // Initialize properties - this.debug = process.env.DEBUG === "true"; - this.baseUrl = process.env.API_BASE_URL || ""; - this.headers = this.parseHeaders(process.env.API_HEADERS || ""); - - // Initialize tools map - do this before creating server - this.initializeTools(); - - // Create MCP server with correct capabilities - this.server = new Server( - { - name: process.env.SERVER_NAME || "${config.name}", - version: process.env.SERVER_VERSION || "${config.version}", - }, - { - capabilities: { - tools: true, // Enable tools capability - }, - } - ); - - // Set up request handlers - don't log here - this.setupHandlers(); - } - - /** - * Parse headers from string - */ - private parseHeaders(headerStr: string): Record { - const headers: Record = {}; - if (headerStr) { - headerStr.split(",").forEach((header) => { - const [key, value] = header.split(":"); - if (key && value) headers[key.trim()] = value.trim(); - }); - } - return headers; - } - - /** - * Initialize tools map from OpenAPI spec - * This runs before the server is connected, so don't log here - */ - private initializeTools(): void { - // Initialize each tool in the tools map - for (const tool of TOOLS) { - this.tools.set(tool.id, { - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema as JsonSchema, - // Don't include security at the tool level - }); - } - - // Don't log here, we're not connected yet - console.error(\`Initialized \${this.tools.size} tools\`); - } - - /** - * Set up request handlers - */ - private setupHandlers(): void { - // Handle tool listing requests - this.server.setRequestHandler(ListToolsRequestSchema, async () => { - this.log('debug', "Handling ListTools request"); - // Return tools in the format expected by MCP SDK - return { - tools: Array.from(this.tools.entries()).map(([id, tool]) => ({ - id, - ...tool, - })), - }; - }); - - // Handle tool execution requests - this.server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { id, name, arguments: params } = request.params; - this.log('debug', "Handling CallTool request", { id, name, params }); - - let toolId: string | undefined; - let toolDetails: OpenApiTool | undefined; - - try { - // Find the requested tool - toolId = id; - if (!toolId && name) { - for (const [tid, tool] of this.tools.entries()) { - if (tool.name === name) { - toolId = tid; - break; - } - } - } - - if (!toolId) { - throw new Error(\`Tool not found: \${id || name}\`); - } - - toolDetails = TOOLS.find(t => t.id === toolId); - if (!toolDetails) { - throw new Error(\`Tool details not found for ID: \${toolId}\`); - } - - this.log('info', \`Executing tool: \${toolId}\`); - - // Execute the API call - const result = await this.executeApiCall(toolDetails, params || {}); - - // Return the result in correct MCP format - return { - content: [ - { - type: "application/json", - data: result - } - ] - }; - - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - this.log('error', \`Error executing tool \${toolId || name}: \${errorMessage}\`); - - // Format error according to MCP SDK expectations - return { - error: { - message: errorMessage, - details: error instanceof Error && 'response' in error - ? JSON.stringify((error as any).response?.data) - : undefined - } - }; - } - }); - } - - /** - * Execute an API call for a tool - */ - private async executeApiCall(tool: OpenApiTool, params: Record): Promise { - // Get method and path from tool - const method = tool.method; - let path = tool.path; - - // Clone params to avoid modifying the original - const requestParams = { ...params }; - - // Replace path parameters with values from params - Object.entries(requestParams).forEach(([key, value]) => { - const placeholder = \`{\${key}}\`; - if (path.includes(placeholder)) { - path = path.replace(placeholder, encodeURIComponent(String(value))); - delete requestParams[key]; // Remove used parameter - } - }); - - // Build the full URL - const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : \`\${this.baseUrl}/\`; - const cleanPath = path.startsWith("/") ? path.slice(1) : path; - const url = new URL(cleanPath, baseUrl).toString(); - - this.log('debug', \`API Request: \${method} \${url}\`); - - try { - // Configure the request - const config: AxiosRequestConfig = { - method: method.toLowerCase(), - url, - headers: { ...this.headers }, - }; - - // Apply security headers based on tool security requirements - if (tool.security && Array.isArray(tool.security)) { - for (const requirement of tool.security) { - for (const securitySchemeName of Object.keys(requirement)) { - const securityDefinition = SECURITY_SCHEMES[securitySchemeName]; - - if (securityDefinition) { - const authType = securityDefinition.type; - - // Handle API key - if (authType === 'apiKey') { - const apiKeyName = securityDefinition.name || ''; - const envVarName = \`\${securitySchemeName.toUpperCase()}_\${apiKeyName.toUpperCase()}\`; - const apiKeyValue = process.env[envVarName]; - - if (apiKeyValue) { - if (securityDefinition.in === 'header') { - config.headers = config.headers || {}; - config.headers[apiKeyName] = apiKeyValue; - } else if (securityDefinition.in === 'query') { - config.params = config.params || {}; - config.params[apiKeyName] = apiKeyValue; - } - } else { - this.log('warning', \`API Key environment variable not found: \${envVarName}\`); - } - } - // Handle bearer token - else if (authType === 'http' && securityDefinition.scheme === 'bearer') { - const envVarName = \`\${securitySchemeName.toUpperCase()}_BEARERTOKEN\`; - const bearerToken = process.env[envVarName]; - - if (bearerToken) { - config.headers = config.headers || {}; - config.headers['Authorization'] = \`Bearer \${bearerToken}\`; - } else { - this.log('warning', \`Bearer Token environment variable not found: \${envVarName}\`); - } - } - // Handle basic auth - else if (authType === 'http' && securityDefinition.scheme === 'basic') { - const username = process.env[\`\${securitySchemeName.toUpperCase()}_USERNAME\`]; - const password = process.env[\`\${securitySchemeName.toUpperCase()}_PASSWORD\`]; - - if (username && password) { - const auth = Buffer.from(\`\${username}:\${password}\`).toString('base64'); - config.headers = config.headers || {}; - config.headers['Authorization'] = \`Basic \${auth}\`; - } else { - this.log('warning', \`Basic auth credentials not found for \${securitySchemeName}\`); - } - } - } - } - } - } - - // Add parameters based on request method - if (["GET", "DELETE"].includes(method)) { - // For GET/DELETE, send params as query string - config.params = { ...(config.params || {}), ...requestParams }; - } else { - // For POST/PUT/PATCH, send params as JSON body - config.data = requestParams; - if (config.headers) { - config.headers["Content-Type"] = "application/json"; - } - } - - this.log('debug', "Request config:", { - url: config.url, - method: config.method, - params: config.params, - headers: config.headers ? Object.keys(config.headers) : [] - }); - - // Execute the request - const response = await axios(config); - this.log('debug', \`Response status: \${response.status}\`); - - return response.data; - - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - this.log('error', \`API request failed: \${errorMessage}\`); - - if (axios.isAxiosError(error)) { - const axiosError = error as AxiosError; - const responseData = axiosError.response?.data; - const responseStatus = axiosError.response?.status; - - this.log('error', 'API Error Details:', { - status: responseStatus, - data: typeof responseData === 'object' ? JSON.stringify(responseData) : String(responseData) - }); - - // Rethrow with more context for better error handling - const detailedError = new Error(\`API request failed with status \${responseStatus}: \${errorMessage}\`); - (detailedError as any).response = axiosError.response; - throw detailedError; - } - - throw error; - } - } - - /** - * Log messages with appropriate level - * Only sends to MCP if we're connected - */ - private log(level: 'debug' | 'info' | 'warning' | 'error', message: string, data?: any): void { - // Always log to stderr for visibility - console.error(\`[\${level.toUpperCase()}] \${message}\${data ? ': ' + JSON.stringify(data) : ''}\`); - - // Only try to send via MCP if we're in debug mode or it's important - if (this.debug || level !== 'debug') { - try { - // Only send if server exists and is connected - if (this.server && (this.server as any).isConnected) { - this.server.sendLoggingMessage({ - level, - data: \`[MCP Server] \${message}\${data ? ': ' + JSON.stringify(data) : ''}\` - }); - } - } catch (e) { - // If logging fails, log to stderr - console.error('Failed to send log via MCP:', (e as Error).message); - } - } - } - - /** - * Start the server - */ - async start(): Promise { - try { - // Create stdio transport - const transport = new StdioServerTransport(); - console.error("MCP Server starting on stdio transport"); - - // Connect to the transport - await this.server.connect(transport); - - // Now we can safely log via MCP - console.error(\`Registered \${this.tools.size} tools\`); - this.log('info', \`MCP Server started successfully with \${this.tools.size} tools\`); - } catch (error) { - console.error("Failed to start MCP server:", error); - process.exit(1); - } - } -} - -// Start the server -async function main(): Promise { - try { - const server = new MCPServer(); - await server.start(); - } catch (error) { - console.error("Failed to start server:", error); - process.exit(1); - } -} - -main(); -`; - - return serverCode; -} - -export { generateServerFile, generateServerTS }; \ No newline at end of file diff --git a/src/tool-generator.js b/src/tool-generator.js deleted file mode 100644 index 7b4c4cb..0000000 --- a/src/tool-generator.js +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Generate a clean tool ID from an API path and method - */ -function generateToolId(method, path) { - // Remove leading slash and parameters - const cleanPath = path.replace(/^\//, '').replace(/[{}]/g, ''); - // Create a clean tool ID - return `${method.toUpperCase()}-${cleanPath}`.replace(/[^a-zA-Z0-9-]/g, '-'); -} - -/** - * Generate tool definitions from OpenAPI paths - */ -function generateTools(spec, verbose = false) { - const toolList = []; - const toolMapObj = {}; - const securitySchemes = spec.components?.securitySchemes || {}; - - // Check if spec.paths exists - if (!spec.paths) { - console.warn("Warning: No paths found in OpenAPI specification"); - return { tools: toolList, toolMap: toolMapObj, securitySchemes }; - } - - console.log(`Processing ${Object.keys(spec.paths).length} API paths...`); - - for (const [path, pathItem] of Object.entries(spec.paths)) { - if (!pathItem) continue; - - for (const [method, operation] of Object.entries(pathItem)) { - if (method === 'parameters' || !operation || typeof method !== 'string') continue; - - // Skip if not a valid HTTP method - const validMethods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head']; - if (!validMethods.includes(method.toLowerCase())) continue; - - const op = operation; - // Get a unique ID for this tool - const toolId = generateToolId(method, path); - // Create a friendly name - const toolName = op.operationId || op.summary || `${method.toUpperCase()} ${path}`; - - if (verbose) { - console.log(`Processing endpoint: ${method.toUpperCase()} ${path} -> Tool ID: ${toolId}`); - } - - const tool = { - id: toolId, - name: toolName, - description: op.description || `Make a ${method.toUpperCase()} request to ${path}`, - method: method.toUpperCase(), - path: path, - inputSchema: { - type: 'object', - properties: {}, - required: [] - }, - security: op.security || spec.security || [] // Get security requirements for the operation or spec - }; - - // Add parameters from operation - if (op.parameters) { - for (const param of op.parameters) { - if ('name' in param && 'in' in param) { - const paramSchema = param.schema; - - // Add parameter to the schema - tool.inputSchema.properties[param.name] = { - type: paramSchema?.type || 'string', - description: param.description || `${param.name} parameter`, - }; - - // Add enum values if present - if (paramSchema?.enum) { - tool.inputSchema.properties[param.name].enum = paramSchema.enum; - } - - // Add required flag if needed - if (param.required) { - tool.inputSchema.required.push(param.name); - } - } - } - } - - // Handle request body for POST/PUT/PATCH methods - if (['post', 'put', 'patch'].includes(method.toLowerCase()) && op.requestBody) { - const contentType = op.requestBody.content?.['application/json']; - - if (contentType && contentType.schema) { - const bodySchema = contentType.schema; - - // Add body properties to the tool's input schema - if (bodySchema.properties) { - for (const [propName, propSchema] of Object.entries(bodySchema.properties)) { - tool.inputSchema.properties[propName] = { - type: propSchema.type || 'string', - description: propSchema.description || `${propName} property`, - }; - - // Add enum values if present - if (propSchema.enum) { - tool.inputSchema.properties[propName].enum = propSchema.enum; - } - } - - // Add required properties - if (bodySchema.required && Array.isArray(bodySchema.required)) { - tool.inputSchema.required.push(...bodySchema.required); - } - } - } - } - - toolList.push(tool); - toolMapObj[toolId] = tool; - } - } - - console.log(`Generated ${toolList.length} MCP tools from the OpenAPI spec`); - return { tools: toolList, toolMap: toolMapObj, securitySchemes }; // return securitySchemes as well -} - -export { generateToolId, generateTools }; \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..7b3ee00 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,40 @@ +export function titleCase(str: string): string { + // Converts snake_case, kebab-case, or path/parts to TitleCase + return str + .toLowerCase() + .replace(/[-_\/](.)/g, (_, char) => char.toUpperCase()) // Handle separators + .replace(/^{/, '') // Remove leading { from path params + .replace(/}$/, '') // Remove trailing } from path params + .replace(/^./, (char) => char.toUpperCase()); // Capitalize first letter +} + +export function generateOperationId(method: string, path: string): string { + // Generator: get /users/{userId}/posts -> GetUsersPostsByUserId + const parts = path.split('/').filter(p => p); // Split and remove empty parts + + let name = method.toLowerCase(); // Start with method name + + parts.forEach((part, index) => { + if (part.startsWith('{') && part.endsWith('}')) { + // Append 'By' + ParamName only for the *last* path parameter segment + if (index === parts.length - 1) { + name += 'By' + titleCase(part); + } + // Potentially include non-terminal params differently if needed, e.g.: + // else { name += 'With' + titleCase(part); } + } else { + // Append the static path part in TitleCase + name += titleCase(part); + } + }); + + // Simple fallback if name is just the method (e.g., GET /) + if (name === method.toLowerCase()) { + name += 'Root'; + } + + // Ensure first letter is uppercase after potential lowercase method start + name = name.charAt(0).toUpperCase() + name.slice(1); + + return name; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..28933fb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file