From 1bd4fd6d9cebaa6dd670160d4168a1c224a5e82a Mon Sep 17 00:00:00 2001 From: samanhappy Date: Mon, 16 Jun 2025 17:50:51 +0800 Subject: [PATCH] feat: Add OpenAPI support with comprehensive configuration options and client integration (#184) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/openapi-schema-support.md | 149 +++++++ docs/openapi-support.md | 172 +++++++++ examples/openapi-schema-config.json | 256 ++++++++++++ frontend/src/components/ServerForm.tsx | 514 +++++++++++++++++++++---- frontend/src/locales/en.json | 28 +- frontend/src/locales/zh.json | 28 +- frontend/src/types/index.ts | 68 +++- package.json | 3 + pnpm-lock.yaml | 119 ++++++ src/clients/openapi.ts | 343 +++++++++++++++++ src/controllers/serverController.ts | 50 ++- src/services/mcpService.ts | 149 ++++++- src/types/index.ts | 46 ++- test-integration.ts | 177 +++++++++ test-openapi-schema.ts | 216 +++++++++++ test-openapi.ts | 64 +++ 16 files changed, 2284 insertions(+), 98 deletions(-) create mode 100644 docs/openapi-schema-support.md create mode 100644 docs/openapi-support.md create mode 100644 examples/openapi-schema-config.json create mode 100644 src/clients/openapi.ts create mode 100644 test-integration.ts create mode 100644 test-openapi-schema.ts create mode 100644 test-openapi.ts diff --git a/docs/openapi-schema-support.md b/docs/openapi-schema-support.md new file mode 100644 index 0000000..4c494b6 --- /dev/null +++ b/docs/openapi-schema-support.md @@ -0,0 +1,149 @@ +# OpenAPI Schema Support in MCPHub + +MCPHub now supports both OpenAPI specification URLs and complete JSON schemas for OpenAPI server configuration. This allows you to either reference an external OpenAPI specification file or embed the complete schema directly in your configuration. + +## Configuration Options + +### 1. Using OpenAPI Specification URL (Traditional) + +```json +{ + "type": "openapi", + "openapi": { + "url": "https://api.example.com/openapi.json", + "version": "3.1.0", + "security": { + "type": "apiKey", + "apiKey": { + "name": "X-API-Key", + "in": "header", + "value": "your-api-key" + } + } + } +} +``` + +### 2. Using Complete JSON Schema (New) + +```json +{ + "type": "openapi", + "openapi": { + "schema": { + "openapi": "3.1.0", + "info": { + "title": "My API", + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://api.example.com" + } + ], + "paths": { + "/users": { + "get": { + "operationId": "getUsers", + "summary": "Get all users", + "responses": { + "200": { + "description": "List of users" + } + } + } + } + } + }, + "version": "3.1.0", + "security": { + "type": "apiKey", + "apiKey": { + "name": "X-API-Key", + "in": "header", + "value": "your-api-key" + } + } + } +} +``` + +## Benefits of JSON Schema Support + +1. **Offline Development**: No need for external URLs during development +2. **Version Control**: Schema changes can be tracked in your configuration +3. **Security**: No external dependencies or network calls required +4. **Customization**: Full control over the API specification +5. **Testing**: Easy to create test configurations with mock schemas + +## Frontend Form Support + +The web interface now includes: + +- **Input Mode Selection**: Choose between "Specification URL" or "JSON Schema" +- **URL Input**: Traditional URL input field for external specifications +- **Schema Editor**: Large text area with syntax highlighting for JSON schema input +- **Validation**: Client-side JSON validation before submission +- **Help Text**: Contextual help for schema format + +## API Validation + +The backend validates that: + +- At least one of `url` or `schema` is provided for OpenAPI servers +- JSON schemas are properly formatted when provided +- Security configurations are valid for both input modes +- All required OpenAPI fields are present + +## Migration Guide + +### From URL to Schema + +If you want to convert an existing URL-based configuration to schema-based: + +1. Download your OpenAPI specification from the URL +2. Copy the JSON content +3. Update your configuration to use the `schema` field instead of `url` +4. Paste the JSON content as the value of the `schema` field + +### Maintaining Both + +You can include both `url` and `schema` in your configuration. The system will prioritize the `schema` field if both are present. + +## Examples + +See the `examples/openapi-schema-config.json` file for complete configuration examples showing both URL and schema-based configurations. + +## Technical Implementation + +- **Backend**: OpenAPI client supports both SwaggerParser.dereference() with URLs and direct schema objects +- **Frontend**: Dynamic form rendering based on selected input mode +- **Validation**: Enhanced validation logic in server controllers +- **Type Safety**: Updated TypeScript interfaces for both input modes + +## Security Considerations + +When using JSON schemas: + +- Ensure schemas are properly validated before use +- Be aware that large schemas increase configuration file size +- Consider using URL-based approach for frequently changing APIs +- Store sensitive information (like API keys) in environment variables, not in schemas + +## Troubleshooting + +### Common Issues + +1. **Invalid JSON**: Ensure your schema is valid JSON format +2. **Missing Required Fields**: OpenAPI schemas must include `openapi`, `info`, and `paths` fields +3. **Schema Size**: Very large schemas may impact performance +4. **Server Configuration**: Ensure the `servers` field in your schema points to the correct endpoints + +### Validation Errors + +The system provides detailed error messages for: + +- Malformed JSON in schema field +- Missing required OpenAPI fields +- Invalid security configurations +- Network issues with URL-based configurations diff --git a/docs/openapi-support.md b/docs/openapi-support.md new file mode 100644 index 0000000..a746270 --- /dev/null +++ b/docs/openapi-support.md @@ -0,0 +1,172 @@ +# OpenAPI Support in MCPHub + +MCPHub now supports OpenAPI 3.1.1 servers as a new server type, allowing you to integrate REST APIs directly into your MCP workflow. + +## Features + +- ✅ **Full OpenAPI 3.1.1 Support**: Load and parse OpenAPI specifications +- ✅ **Multiple Security Types**: None, API Key, HTTP Authentication, OAuth 2.0, OpenID Connect +- ✅ **Dynamic Tool Generation**: Automatically creates MCP tools from OpenAPI operations +- ✅ **Type Safety**: Full TypeScript support with proper type definitions +- ✅ **Frontend Integration**: Easy-to-use forms for configuring OpenAPI servers +- ✅ **Internationalization**: Support for English and Chinese languages + +## Configuration + +### Basic Configuration + +```json +{ + "type": "openapi", + "openapi": { + "url": "https://api.example.com/v1/openapi.json", + "version": "3.1.0", + "security": { + "type": "none" + } + } +} +``` + +### With API Key Authentication + +```json +{ + "type": "openapi", + "openapi": { + "url": "https://api.example.com/v1/openapi.json", + "version": "3.1.0", + "security": { + "type": "apiKey", + "apiKey": { + "name": "X-API-Key", + "in": "header", + "value": "your-api-key-here" + } + } + }, + "headers": { + "Accept": "application/json" + } +} +``` + +### With HTTP Bearer Authentication + +```json +{ + "type": "openapi", + "openapi": { + "url": "https://api.example.com/v1/openapi.json", + "version": "3.1.0", + "security": { + "type": "http", + "http": { + "scheme": "bearer", + "credentials": "your-bearer-token-here" + } + } + } +} +``` + +## Supported Security Types + +1. **None**: No authentication required +2. **API Key**: API key in header or query parameter +3. **HTTP**: Basic, Bearer, or Digest authentication +4. **OAuth 2.0**: OAuth 2.0 access tokens +5. **OpenID Connect**: OpenID Connect ID tokens + +## How It Works + +1. **Specification Loading**: The OpenAPI client fetches and parses the OpenAPI specification +2. **Tool Generation**: Each operation in the spec becomes an MCP tool +3. **Request Handling**: Tools handle parameter validation and API calls +4. **Response Processing**: API responses are returned as tool results + +## Frontend Usage + +1. Navigate to the Servers page +2. Click "Add Server" +3. Select "OpenAPI" as the server type +4. Enter the OpenAPI specification URL +5. Configure security settings if needed +6. Add any additional headers +7. Save the configuration + +## Testing + +You can test the OpenAPI integration using the provided test scripts: + +```bash +# Test OpenAPI client directly +npx tsx test-openapi.ts + +# Test full integration +npx tsx test-integration.ts +``` + +## Example: Swagger Petstore + +The Swagger Petstore API is a perfect example for testing: + +```json +{ + "type": "openapi", + "openapi": { + "url": "https://petstore3.swagger.io/api/v3/openapi.json", + "version": "3.1.0", + "security": { + "type": "none" + } + } +} +``` + +This will create tools like: + +- `addPet`: Add a new pet to the store +- `findPetsByStatus`: Find pets by status +- `getPetById`: Find pet by ID +- And many more... + +## Error Handling + +The OpenAPI client includes comprehensive error handling: + +- Network errors are properly caught and reported +- Invalid specifications are rejected with clear error messages +- API errors include response status and body information +- Type validation ensures proper parameter handling + +## Limitations + +- Only supports OpenAPI 3.x specifications (3.0.0 and above) +- Complex authentication flows (like OAuth 2.0 authorization code flow) require manual token management +- Large specifications may take time to parse initially +- Some advanced OpenAPI features may not be fully supported + +## Contributing + +To add new features or fix bugs in the OpenAPI integration: + +1. Backend types: `src/types/index.ts` +2. OpenAPI client: `src/clients/openapi.ts` +3. Service integration: `src/services/mcpService.ts` +4. Frontend forms: `frontend/src/components/ServerForm.tsx` +5. Internationalization: `frontend/src/locales/` + +## Troubleshooting + +**Q: My OpenAPI server won't connect** +A: Check that the specification URL is accessible and returns valid JSON/YAML + +**Q: Tools aren't showing up** +A: Verify that your OpenAPI specification includes valid operations with required fields + +**Q: Authentication isn't working** +A: Double-check your security configuration matches the API's requirements + +**Q: Getting CORS errors** +A: The API server needs to allow CORS requests from your MCPHub domain diff --git a/examples/openapi-schema-config.json b/examples/openapi-schema-config.json new file mode 100644 index 0000000..432bf8a --- /dev/null +++ b/examples/openapi-schema-config.json @@ -0,0 +1,256 @@ +{ + "mcpServers": { + "example-api-url": { + "type": "openapi", + "openapi": { + "url": "https://api.example.com/openapi.json", + "version": "3.1.0", + "security": { + "type": "apiKey", + "apiKey": { + "name": "X-API-Key", + "in": "header", + "value": "your-api-key-here" + } + } + }, + "headers": { + "User-Agent": "MCPHub/1.0" + }, + "enabled": true + }, + "example-api-schema": { + "type": "openapi", + "openapi": { + "schema": { + "openapi": "3.1.0", + "info": { + "title": "Example API", + "version": "1.0.0", + "description": "A sample API for demonstration" + }, + "servers": [ + { + "url": "https://api.example.com", + "description": "Production server" + } + ], + "paths": { + "/users": { + "get": { + "operationId": "listUsers", + "summary": "List all users", + "description": "Retrieve a list of all users in the system", + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "Maximum number of users to return", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 10 + } + }, + { + "name": "offset", + "in": "query", + "description": "Number of users to skip", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0 + } + } + ], + "responses": { + "200": { + "description": "List of users", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "users": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + }, + "total": { + "type": "integer", + "description": "Total number of users" + } + } + } + } + } + } + } + }, + "post": { + "operationId": "createUser", + "summary": "Create a new user", + "description": "Create a new user in the system", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserRequest" + } + } + } + }, + "responses": { + "201": { + "description": "User created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + }, + "/users/{userId}": { + "get": { + "operationId": "getUserById", + "summary": "Get user by ID", + "description": "Retrieve a specific user by their ID", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "description": "ID of the user to retrieve", + "schema": { + "type": "integer", + "minimum": 1 + } + } + ], + "responses": { + "200": { + "description": "User details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "404": { + "description": "User not found" + } + } + } + } + }, + "components": { + "schemas": { + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Unique identifier for the user" + }, + "name": { + "type": "string", + "description": "Full name of the user" + }, + "email": { + "type": "string", + "format": "email", + "description": "Email address of the user" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the user was created" + }, + "status": { + "type": "string", + "enum": [ + "active", + "inactive", + "suspended" + ], + "description": "Current status of the user" + } + }, + "required": [ + "id", + "name", + "email" + ] + }, + "CreateUserRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Full name of the user" + }, + "email": { + "type": "string", + "format": "email", + "description": "Email address of the user" + }, + "status": { + "type": "string", + "enum": [ + "active", + "inactive" + ], + "default": "active", + "description": "Initial status of the user" + } + }, + "required": [ + "name", + "email" + ] + } + }, + "securitySchemes": { + "ApiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "version": "3.1.0", + "security": { + "type": "apiKey", + "apiKey": { + "name": "X-API-Key", + "in": "header", + "value": "your-api-key-here" + } + } + }, + "headers": { + "User-Agent": "MCPHub/1.0" + }, + "enabled": true + } + } +} \ No newline at end of file diff --git a/frontend/src/components/ServerForm.tsx b/frontend/src/components/ServerForm.tsx index 43aef04..e040d70 100644 --- a/frontend/src/components/ServerForm.tsx +++ b/frontend/src/components/ServerForm.tsx @@ -26,7 +26,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr } }; - const [serverType, setServerType] = useState<'stdio' | 'sse' | 'streamable-http'>(getInitialServerType()); + const [serverType, setServerType] = useState<'stdio' | 'sse' | 'streamable-http' | 'openapi'>(getInitialServerType()); const [formData, setFormData] = useState({ name: (initialData && initialData.name) || '', @@ -46,6 +46,32 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr timeout: (initialData && initialData.config && initialData.config.options && initialData.config.options.timeout) || 60000, resetTimeoutOnProgress: (initialData && initialData.config && initialData.config.options && initialData.config.options.resetTimeoutOnProgress) || false, maxTotalTimeout: (initialData && initialData.config && initialData.config.options && initialData.config.options.maxTotalTimeout) || undefined, + }, + // OpenAPI configuration initialization + openapi: initialData && initialData.config && initialData.config.openapi ? { + url: initialData.config.openapi.url || '', + schema: initialData.config.openapi.schema ? JSON.stringify(initialData.config.openapi.schema, null, 2) : '', + inputMode: initialData.config.openapi.url ? 'url' : (initialData.config.openapi.schema ? 'schema' : 'url'), + version: initialData.config.openapi.version || '3.1.0', + securityType: initialData.config.openapi.security?.type || 'none', + // API Key initialization + apiKeyName: initialData.config.openapi.security?.apiKey?.name || '', + apiKeyIn: initialData.config.openapi.security?.apiKey?.in || 'header', + apiKeyValue: initialData.config.openapi.security?.apiKey?.value || '', + // HTTP auth initialization + httpScheme: initialData.config.openapi.security?.http?.scheme || 'bearer', + httpCredentials: initialData.config.openapi.security?.http?.credentials || '', + // OAuth2 initialization + oauth2Token: initialData.config.openapi.security?.oauth2?.token || '', + // OpenID Connect initialization + openIdConnectUrl: initialData.config.openapi.security?.openIdConnect?.url || '', + openIdConnectToken: initialData.config.openapi.security?.openIdConnect?.token || '' + } : { + inputMode: 'url', + url: '', + schema: '', + version: '3.1.0', + securityType: 'none' } }) @@ -76,7 +102,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr setFormData({ ...formData, arguments: value, args }) } - const updateServerType = (type: 'stdio' | 'sse' | 'streamable-http') => { + const updateServerType = (type: 'stdio' | 'sse' | 'streamable-http' | 'openapi') => { setServerType(type); setFormData(prev => ({ ...prev, type })); } @@ -160,16 +186,69 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr name: formData.name, config: { type: serverType, // Always include the type - ...(serverType === 'sse' || serverType === 'streamable-http' + ...(serverType === 'openapi' ? { - url: formData.url, + openapi: (() => { + const openapi: any = { + version: formData.openapi?.version || '3.1.0' + }; + + // Add URL or schema based on input mode + if (formData.openapi?.inputMode === 'url') { + openapi.url = formData.openapi?.url || ''; + } else if (formData.openapi?.inputMode === 'schema' && formData.openapi?.schema) { + try { + openapi.schema = JSON.parse(formData.openapi.schema); + } catch (e) { + throw new Error('Invalid JSON schema format'); + } + } + + // Add security configuration if provided + if (formData.openapi?.securityType && formData.openapi.securityType !== 'none') { + openapi.security = { + type: formData.openapi.securityType, + ...(formData.openapi.securityType === 'apiKey' && { + apiKey: { + name: formData.openapi.apiKeyName || '', + in: formData.openapi.apiKeyIn || 'header', + value: formData.openapi.apiKeyValue || '' + } + }), + ...(formData.openapi.securityType === 'http' && { + http: { + scheme: formData.openapi.httpScheme || 'bearer', + credentials: formData.openapi.httpCredentials || '' + } + }), + ...(formData.openapi.securityType === 'oauth2' && { + oauth2: { + token: formData.openapi.oauth2Token || '' + } + }), + ...(formData.openapi.securityType === 'openIdConnect' && { + openIdConnect: { + url: formData.openapi.openIdConnectUrl || '', + token: formData.openapi.openIdConnectToken || '' + } + }) + }; + } + + return openapi; + })(), ...(Object.keys(headers).length > 0 ? { headers } : {}) } - : { - command: formData.command, - args: formData.args, - env: Object.keys(env).length > 0 ? env : undefined, - } + : serverType === 'sse' || serverType === 'streamable-http' + ? { + url: formData.url, + ...(Object.keys(headers).length > 0 ? { headers } : {}) + } + : { + command: formData.command, + args: formData.args, + env: Object.keys(env).length > 0 ? env : undefined, + } ), ...(Object.keys(options).length > 0 ? { options } : {}) } @@ -253,10 +332,291 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr /> +
+ updateServerType('openapi')} + className="mr-1" + /> + +
- {serverType === 'sse' || serverType === 'streamable-http' ? ( + {serverType === 'openapi' ? ( + <> + {/* Input Mode Selection */} +
+ +
+
+ setFormData(prev => ({ + ...prev, + openapi: { ...prev.openapi!, inputMode: 'url' } + }))} + className="mr-1" + /> + +
+
+ setFormData(prev => ({ + ...prev, + openapi: { ...prev.openapi!, inputMode: 'schema' } + }))} + className="mr-1" + /> + +
+
+
+ + {/* URL Input */} + {formData.openapi?.inputMode === 'url' && ( +
+ + setFormData(prev => ({ + ...prev, + openapi: { ...prev.openapi!, url: e.target.value } + }))} + className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" + placeholder="e.g.: https://api.example.com/openapi.json" + required={serverType === 'openapi' && formData.openapi?.inputMode === 'url'} + /> +
+ )} + + {/* Schema Input */} + {formData.openapi?.inputMode === 'schema' && ( +
+ +