# Directory Structure ``` ├── .gitignore ├── .prettierignore ├── .prettierrc ├── example.json ├── example.yaml ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── src │ └── index.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | node_modules/ 2 | build/ 3 | .env* 4 | ``` -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- ``` 1 | node_modules 2 | build 3 | dist 4 | package-lock.json 5 | yarn.lock ``` -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- ``` 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 100, 6 | "tabWidth": 2, 7 | "arrowParens": "always" 8 | } 9 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # OpenAPI MCP Server 2 | 3 | A Quick Way to Create an MCP Server with `StreamableHTTP` transport implementation from OpenAPI docs. 4 | 5 | ## Overview 6 | 7 | This server provides a standardized way to interact with model services through a RESTful API interface. It implements the Model Context Protocol (MCP) and is designed to be easily configurable. **Simply set up your `.env` file, and the server is ready to run.** 8 | 9 | It implements the Model Context Protocol (MCP) specification and supports OpenAPI documentation. 10 | 11 | ## Features 12 | 13 | - OpenAPI 3.0.0 compliant API documentation 14 | - Model service API documentation retrieval 15 | - Model service invocation with parameter handling 16 | - TypeScript implementation for type safety 17 | 18 | ## Prerequisites 19 | 20 | - Node.js (v20 or higher) 21 | - npm (v6 or higher) 22 | 23 | ## Quick Start 24 | 25 | 1. **Clone the repository:** 26 | 27 | ```bash 28 | git clone https://github.com/oneWalker/openapi-mcp-server.git 29 | cd openapi-mcp-server 30 | ``` 31 | 32 | 2. **Install dependencies:** 33 | 34 | ```bash 35 | npm install 36 | ``` 37 | 38 | 3. **Configure your environment:** 39 | Create a `.env` file in the project root and add your configuration. See the [Configuration](#configuration) section for details. 40 | 41 | 4. **Run the server:** 42 | ```bash 43 | npm run build 44 | npm run start 45 | ``` 46 | 47 | ## Installation 48 | 49 | 1. Clone the repository 50 | 2. Install dependencies: 51 | 52 | ```bash 53 | npm install 54 | ``` 55 | 56 | ## Development 57 | 58 | ### Building the Project 59 | 60 | ```bash 61 | npm run build 62 | ``` 63 | 64 | ### Running in Development Mode 65 | 66 | ```bash 67 | npm run watch 68 | ``` 69 | 70 | ### Starting the Server 71 | 72 | ```bash 73 | npm run start 74 | ``` 75 | 76 | ## Configuration 77 | 78 | Create a `.env` file in the root of the project to configure the server. 79 | 80 | ``` 81 | # The base URL for the original API server 82 | BASE_SERVER_URL= https://api.example.com 83 | 84 | # The path to the OpenAPI specification file (can be a local file or a URL). 85 | OPENAPI_PATH=./example.yaml or ./example.json # example.yaml is just for demo 86 | 87 | # The port for the MCP server to run on 88 | PORT=8000 89 | ``` 90 | 91 | ## API Endpoints 92 | 93 | ### Get Model Service API Documentation 94 | 95 | ``` 96 | GET /api/model/services/{ID} 97 | ``` 98 | 99 | Retrieves the API documentation for a specific model service. 100 | 101 | **Parameters:** 102 | 103 | - `ID` (path, required): Model service ID 104 | - `authorization` (header, required): Bearer token for authentication 105 | 106 | ### Call Model Service 107 | 108 | ``` 109 | POST /api/model/services/ 110 | ``` 111 | 112 | Invokes a specific model service with provided parameters. 113 | 114 | **Parameters:** 115 | 116 | - `id` (path, required): Model service ID 117 | 118 | **Request Body:** 119 | 120 | ```json 121 | { 122 | "id": "123" 123 | } 124 | ``` 125 | 126 | ## Project Structure 127 | 128 | ``` 129 | openapi-mcp-server/ 130 | ├── src/ # Source code 131 | ├── build/ # Compiled JavaScript files 132 | ├── example.yaml # OpenAPI specification 133 | ├── package.json # Project configuration 134 | └── tsconfig.json # TypeScript configuration 135 | ``` 136 | 137 | ## Contributing 138 | 139 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 140 | 141 | Please see the `CONTRIBUTING.md` file for details on our code of conduct, and the process for submitting pull requests to us. 142 | 143 | ## Reporting Issues 144 | 145 | We use GitHub Issues to track public bugs. Report a bug by [opening a new issue](https://github.com/oneWalker/openapi-mcp-server/issues); it's that easy! 146 | 147 | ## Dependencies 148 | 149 | - `openapi-mcp-generator`: OpenAPI specification generator 150 | - **Note:** This project requires a pending fix from the `openapi-mcp-generator` library. See this [pull request](https://github.com/harsha-iiiv/openapi-mcp-generator/pull/27). 151 | - `@modelcontextprotocol/sdk`: MCP SDK for protocol implementation 152 | - `express`: Web framework 153 | - `dotenv`: Environment variable management 154 | - `got`: HTTP client 155 | 156 | ## Development Dependencies 157 | 158 | - `TypeScript` 159 | - `@types/express` 160 | - `@types/node` 161 | 162 | ## License 163 | 164 | MIT License - See LICENSE file for details 165 | 166 | ## Authors 167 | 168 | - [Brian,Kun Liu](https://github.com/oneWalker) 169 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } 16 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "openapi-mcp-server", 3 | "version": "0.1.4", 4 | "description": "MCP server for OpenAPI", 5 | "type": "module", 6 | "bin": { 7 | "openapi-mcp": "./build/index.js" 8 | }, 9 | "files": [ 10 | "build" 11 | ], 12 | "scripts": { 13 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", 14 | "prepare": "npm run build", 15 | "watch": "tsc --watch", 16 | "inspector": "npx @modelcontextprotocol/inspector build/index.js", 17 | "prepublishOnly": "npm run build", 18 | "start": "node build/index.js", 19 | "format": "pretty-quick --staged", 20 | "format.check": "prettier --check .", 21 | "format.write": "prettier --write ." 22 | }, 23 | "keywords": [ 24 | "openapi-mcp", 25 | "openapi", 26 | "mcp", 27 | "model-context-protocol", 28 | "ai-tools" 29 | ], 30 | "author": "Brian, Kun Liu", 31 | "license": "MIT", 32 | "dependencies": { 33 | "@modelcontextprotocol/sdk": "^1.12.1", 34 | "dotenv": "^16.4.5", 35 | "express": "^5.1.0", 36 | "got": "^14.4.7", 37 | "openapi-mcp-generator": "^3.1.3", 38 | "raw-body": "^3.0.0" 39 | }, 40 | "devDependencies": { 41 | "@types/express": "^5.0.1", 42 | "@types/node": "^20.11.24", 43 | "prettier": "^3.6.2", 44 | "pretty-quick": "^4.2.2", 45 | "typescript": "^5.3.3" 46 | } 47 | } 48 | ``` -------------------------------------------------------------------------------- /example.yaml: -------------------------------------------------------------------------------- ```yaml 1 | openapi: 3.0.0 2 | info: 3 | title: Sample Service API Documentation 4 | version: 1.0.0 5 | servers: 6 | - url: example.com 7 | description: ModelService API Server 8 | 9 | paths: 10 | /api/model/services/{ID}: 11 | get: 12 | summary: Get model service API documentation 13 | description: Retrieves the API documentation for a specific model service 14 | parameters: 15 | - name: ID 16 | in: path 17 | required: true 18 | schema: 19 | type: string 20 | description: Model service ID 21 | - name: authorization 22 | in: header 23 | required: true 24 | schema: 25 | type: string 26 | pattern: '^Bearer\s[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$' 27 | description: Authorization token 28 | # - name: model_id 29 | # in: query 30 | # required: true 31 | # schema: 32 | # type: string 33 | # description: Model ID 34 | # requestBody: 35 | # required: true 36 | # content: 37 | # application/json: 38 | # schema: 39 | # type: object 40 | # properties: 41 | # model_version: 42 | # type: string 43 | # description: Model version 44 | responses: 45 | '200': 46 | description: Successful response 47 | content: 48 | application/json: 49 | schema: 50 | type: object 51 | properties: 52 | Descriptions: 53 | type: array 54 | items: 55 | type: object 56 | properties: 57 | Title: 58 | type: string 59 | description: Title of the API documentation section 60 | Content: 61 | type: string 62 | description: Content of the API documentation section 63 | /api/model/services/: 64 | post: 65 | summary: Call model service 66 | description: Invokes a specific model service with provided parameters 67 | parameters: 68 | - name: id 69 | in: path 70 | required: true 71 | schema: 72 | type: string 73 | description: Model service ID 74 | requestBody: 75 | required: true 76 | content: 77 | application/json: 78 | schema: 79 | type: object 80 | properties: 81 | content: 82 | type: object 83 | properties: 84 | input: 85 | type: object 86 | description: Input parameters for the model service 87 | responses: 88 | '200': 89 | description: Successful response 90 | content: 91 | application/json: 92 | schema: 93 | type: object 94 | properties: 95 | output: 96 | type: string 97 | description: Output from the model service 98 | time: 99 | type: number 100 | description: Processing time in milliseconds 101 | ``` -------------------------------------------------------------------------------- /example.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "Sample Service API Documentation", 5 | "version": "1.0.0" 6 | }, 7 | "servers": [ 8 | { 9 | "url": "example.com", 10 | "description": "ModelService API Server" 11 | } 12 | ], 13 | "paths": { 14 | "/api/model/services/{ID}": { 15 | "get": { 16 | "summary": "Get model service API documentation", 17 | "description": "Retrieves the API documentation for a specific model service", 18 | "parameters": [ 19 | { 20 | "name": "ID", 21 | "in": "path", 22 | "required": true, 23 | "schema": { 24 | "type": "string" 25 | }, 26 | "description": "Model service ID" 27 | } 28 | ], 29 | "responses": { 30 | "200": { 31 | "description": "Successful response", 32 | "content": { 33 | "application/json": { 34 | "schema": { 35 | "type": "object", 36 | "properties": { 37 | "Descriptions": { 38 | "type": "array", 39 | "items": { 40 | "type": "object", 41 | "properties": { 42 | "Title": { 43 | "type": "string", 44 | "description": "Title of the API documentation section" 45 | }, 46 | "Content": { 47 | "type": "string", 48 | "description": "Content of the API documentation section" 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | }, 61 | "/api/model/services/{id}": { 62 | "post": { 63 | "summary": "Call model service", 64 | "description": "Invokes a specific model service with provided parameters", 65 | "parameters": [ 66 | { 67 | "name": "id", 68 | "in": "path", 69 | "required": true, 70 | "schema": { 71 | "type": "string" 72 | }, 73 | "description": "Model service ID" 74 | } 75 | ], 76 | "requestBody": { 77 | "required": true, 78 | "content": { 79 | "application/json": { 80 | "schema": { 81 | "type": "object", 82 | "properties": { 83 | "content": { 84 | "type": "object", 85 | "properties": { 86 | "input": { 87 | "type": "object", 88 | "description": "Input parameters for the model service" 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | }, 97 | "responses": { 98 | "200": { 99 | "description": "Successful response", 100 | "content": { 101 | "application/json": { 102 | "schema": { 103 | "type": "object", 104 | "properties": { 105 | "output": { 106 | "type": "string", 107 | "description": "Output from the model service" 108 | }, 109 | "time": { 110 | "type": "number", 111 | "description": "Processing time in milliseconds" 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } 122 | } 123 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | import express, { Request, Response } from 'express'; 4 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 5 | import { 6 | CallToolRequestSchema, 7 | ListToolsRequestSchema, 8 | Tool, 9 | McpError, 10 | ErrorCode, 11 | } from '@modelcontextprotocol/sdk/types.js'; 12 | import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; 13 | import { getToolsFromOpenApi, McpToolDefinition, GetToolsOptions } from 'openapi-mcp-generator'; 14 | import dotenv from 'dotenv'; 15 | import got, { OptionsInit as GotOptionsInit, Method } from 'got'; 16 | 17 | dotenv.config(); 18 | 19 | const baseServerUrl = process.env.BASE_SERVER_URL; 20 | const openapiPath = process.env.OPENAPI_PATH; 21 | 22 | if (!baseServerUrl || !openapiPath) { 23 | console.error('BASE_SERVER_URL and OPENAPI_PATH must be set'); 24 | process.exit(1); 25 | } 26 | 27 | interface OpenAPITool extends McpToolDefinition { 28 | function?: (args: any) => Promise<any>; 29 | } 30 | 31 | class OpenAPIClient { 32 | private server: Server; 33 | private tools: OpenAPITool[] = []; 34 | 35 | constructor() { 36 | this.server = new Server( 37 | { 38 | name: 'openapi-mcp', 39 | version: '0.1.0', 40 | }, 41 | { 42 | capabilities: { 43 | resources: {}, 44 | tools: {}, 45 | prompts: {}, 46 | }, 47 | }, 48 | ); 49 | 50 | this.setupHandlers(); 51 | this.setupErrorHandling(); 52 | } 53 | 54 | private setupErrorHandling(): void { 55 | this.server.onerror = (error) => { 56 | console.error('[MCP Error]', error); 57 | }; 58 | 59 | process.on('SIGINT', async () => { 60 | await this.server.close(); 61 | process.exit(0); 62 | }); 63 | } 64 | 65 | private setupHandlers(): void { 66 | this.setupToolHandlers(); 67 | } 68 | 69 | private setupToolHandlers(): void { 70 | this.server.setRequestHandler(ListToolsRequestSchema, async () => { 71 | return { tools: this.tools }; 72 | }); 73 | 74 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 75 | try { 76 | const tool = this.tools.find((t) => t.name === request.params.name); 77 | if (!tool) { 78 | throw new McpError(ErrorCode.InvalidRequest, `Tool ${request.params.name} not found`); 79 | } 80 | 81 | const args = request.params.arguments ?? {}; 82 | 83 | const jsonBody = args.requestBody; 84 | const headers: Record<string, string> = {}; 85 | const searchParams: Record<string, string> = {}; 86 | let path = tool.pathTemplate as string; 87 | (tool.parameters as any[]).forEach((param: any) => { 88 | switch (param.in) { 89 | case 'path': 90 | //replace the path with the args, matched `{param.name}` or ':id' 91 | path = path.replace(`{${param.name}}`, args[param.name] as string); 92 | path = path.replace(`:${param.name}`, args[param.name] as string); 93 | break; 94 | case 'query': 95 | searchParams[param.name] = args[param.name] as string; 96 | break; 97 | case 'header': 98 | headers[param.name] = args[param.name] as string; 99 | break; 100 | default: 101 | console.error('Unknown parameter type:', param.in); 102 | break; 103 | } 104 | }); 105 | 106 | // Call the tool function with the provided arguments 107 | const result = await tool.function?.({ 108 | path, 109 | headers, 110 | searchParams: new URLSearchParams(searchParams), 111 | jsonBody, 112 | }); 113 | 114 | const resultText = typeof result === 'object' ? JSON.stringify(result, null, 2) : result; 115 | 116 | return { 117 | content: [ 118 | { 119 | type: 'text', 120 | text: resultText, 121 | }, 122 | ], 123 | }; 124 | } catch (error: any) { 125 | console.error(`Error executing tool ${request.params.name}:`, error); 126 | throw new McpError(ErrorCode.InternalError, `Failed to execute tool: ${error.message}`); 127 | } 128 | }); 129 | } 130 | 131 | private async loadTools(): Promise<void> { 132 | try { 133 | const config: GetToolsOptions = { 134 | baseUrl: baseServerUrl, 135 | dereference: true, 136 | excludeOperationIds: [], 137 | filterFn: (tool: McpToolDefinition) => true, 138 | }; 139 | 140 | const rawTools = await getToolsFromOpenApi(openapiPath as string, config); 141 | 142 | // Transform the tools to include HTTP request functionality 143 | this.tools = rawTools.map((tool: OpenAPITool) => ({ 144 | ...tool, 145 | function: async (args: { 146 | path: string; 147 | headers: Record<string, string>; 148 | searchParams: Record<string, string>; 149 | jsonBody: Record<string, any>; 150 | }) => { 151 | const { path, headers, searchParams, jsonBody } = args; 152 | const method = tool.method.toLowerCase() as Method; 153 | 154 | const url = new URL(path, config.baseUrl); 155 | 156 | try { 157 | const gotOptions: GotOptionsInit = { 158 | method, 159 | headers, 160 | searchParams, 161 | }; 162 | if (method !== 'get') { 163 | gotOptions.json = jsonBody; 164 | } 165 | const response = await got(url, gotOptions); 166 | 167 | return (response as any).body; 168 | } catch (error: any) { 169 | console.error(`HTTP request failed for ${tool.name}:`, error); 170 | throw new McpError(ErrorCode.InternalError, `HTTP request failed: ${error.message}`); 171 | } 172 | }, 173 | })) as OpenAPITool[]; 174 | } catch (error) { 175 | console.error('Error loading tools from OpenAPI:', error); 176 | throw error; 177 | } 178 | } 179 | 180 | async run(): Promise<void> { 181 | await this.loadTools(); 182 | const app = express(); 183 | app.use(express.json()); 184 | 185 | app.post('/mcp', async (req: Request, res: Response) => { 186 | // In stateless mode, create a new instance of transport and server for each request 187 | // to ensure complete isolation. A single instance would cause request ID collisions 188 | // when multiple clients connect concurrently. 189 | 190 | try { 191 | const client = new OpenAPIClient(); 192 | const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ 193 | sessionIdGenerator: undefined, 194 | }); 195 | res.on('close', () => { 196 | console.log('Request closed'); 197 | transport.close(); 198 | client.server.close(); 199 | }); 200 | await client.server.connect(transport); 201 | 202 | const authHeader = req.headers.authorization; 203 | const token = authHeader?.split(' ')[1]; 204 | if (!req.body.params) { 205 | req.body.params = {}; 206 | } 207 | req.body.params.context = { token }; 208 | await transport.handleRequest(req, res, req.body); 209 | } catch (error) { 210 | console.error('Error handling MCP request:', error); 211 | if (!res.headersSent) { 212 | res.status(500).json({ 213 | jsonrpc: '2.0', 214 | error: { 215 | code: -32603, 216 | message: 'Internal server error', 217 | }, 218 | id: null, 219 | }); 220 | } 221 | } 222 | }); 223 | 224 | const port = process.env.PORT || 8000; 225 | console.error('OpenAPI MCP server running on http, port:', port); 226 | app.listen(port); 227 | // const transport = new StdioServerTransport(); 228 | // await this.server.connect(transport); 229 | // console.error("Tavily MCP server running on stdio"); 230 | } 231 | } 232 | 233 | // Start the server 234 | const client = new OpenAPIClient(); 235 | client.run().catch((error) => { 236 | console.error('Failed to run server:', error); 237 | process.exit(1); 238 | }); 239 | ```