├── .gitignore ├── src ├── version.ts.template ├── resources │ ├── response-format.md │ ├── config.md │ └── examples.md └── index.ts ├── tsconfig.json ├── .github ├── workflows │ ├── refresh-badges.yml │ ├── pr-validation.yml │ └── publish.yml ├── conventional-changelog.config.js └── conventional-changelog.config.cjs ├── commitlint.config.js ├── LICENSE ├── CHANGELOG.md ├── Dockerfile ├── package.json ├── scripts └── build.js ├── smithery.yaml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | src/version.ts 4 | .DS_Store 5 | *.log 6 | -------------------------------------------------------------------------------- /src/version.ts.template: -------------------------------------------------------------------------------- 1 | // Template file for TypeScript during development 2 | // The actual version.ts will be generated during build 3 | 4 | export const VERSION = '0.0.0'; 5 | export const PACKAGE_NAME = 'dkmaker-mcp-rest-api'; 6 | export const SERVER_NAME = 'rest-api'; 7 | -------------------------------------------------------------------------------- /tsconfig.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 | -------------------------------------------------------------------------------- /.github/workflows/refresh-badges.yml: -------------------------------------------------------------------------------- 1 | name: Refresh Badges 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' # Run at 00:00 UTC every day 6 | push: 7 | branches: 8 | - main 9 | workflow_dispatch: # Allow manual triggering 10 | 11 | jobs: 12 | refresh: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Refresh badges 16 | uses: b3b00/refreshBadgesAction@v1.0.7 17 | with: 18 | repository: 'zenturacp/mcp-rest-api' 19 | branch: 'main' 20 | -------------------------------------------------------------------------------- /.github/conventional-changelog.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | types: [ 3 | { type: 'feat', section: 'Features' }, 4 | { type: 'fix', section: 'Bug Fixes' }, 5 | { type: 'chore', section: 'Maintenance' }, 6 | { type: 'docs', section: 'Documentation' }, 7 | { type: 'style', section: 'Styling' }, 8 | { type: 'refactor', section: 'Code Refactoring' }, 9 | { type: 'perf', section: 'Performance' }, 10 | { type: 'test', section: 'Testing' }, 11 | { type: 'ci', section: 'CI/CD' }, 12 | { type: 'build', section: 'Build System' } 13 | ] 14 | }; 15 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'type-enum': [ 5 | 2, 6 | 'always', 7 | [ 8 | 'build', 9 | 'chore', 10 | 'ci', 11 | 'docs', 12 | 'feat', 13 | 'fix', 14 | 'perf', 15 | 'refactor', 16 | 'revert', 17 | 'style', 18 | 'test' 19 | ] 20 | ], 21 | 'type-case': [2, 'always', 'lower-case'], 22 | 'type-empty': [2, 'never'], 23 | 'scope-case': [2, 'always', 'lower-case'], 24 | 'subject-case': [2, 'always', 'lower-case'], 25 | 'subject-empty': [2, 'never'], 26 | 'subject-full-stop': [2, 'never', '.'] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/pr-validation.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Validation 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | validate: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: '18' 19 | cache: 'npm' 20 | 21 | - name: Install dependencies 22 | run: npm ci 23 | 24 | - name: Validate Conventional Commits 25 | run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose 26 | 27 | - name: Build 28 | run: npm run build 29 | 30 | - name: Run Tests 31 | run: | 32 | if [ -f "package.json" ] && grep -q "\"test\":" "package.json"; then 33 | npm test 34 | else 35 | echo "No test script found in package.json" 36 | fi 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Christian Pedersen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/conventional-changelog.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | types: [ 3 | { type: 'feat', section: 'Features', hidden: false }, 4 | { type: 'fix', section: 'Bug Fixes', hidden: false }, 5 | { type: 'chore', section: 'Maintenance', hidden: false }, 6 | { type: 'docs', section: 'Documentation', hidden: false }, 7 | { type: 'style', section: 'Styling', hidden: false }, 8 | { type: 'refactor', section: 'Code Refactoring', hidden: false }, 9 | { type: 'perf', section: 'Performance', hidden: false }, 10 | { type: 'test', section: 'Testing', hidden: false }, 11 | { type: 'ci', section: 'CI/CD', hidden: false }, 12 | { type: 'build', section: 'Build System', hidden: false } 13 | ], 14 | releaseRules: [ 15 | { type: 'feat', release: 'minor' }, 16 | { type: 'fix', release: 'patch' }, 17 | { type: 'perf', release: 'patch' }, 18 | { type: 'chore', release: 'patch' }, 19 | { type: 'docs', release: 'patch' }, 20 | { type: 'style', release: 'patch' }, 21 | { type: 'refactor', release: 'patch' }, 22 | { type: 'test', release: 'patch' }, 23 | { type: 'ci', release: 'patch' }, 24 | { type: 'build', release: 'patch' } 25 | ] 26 | }; 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.4.0](https://github.com/dkmaker/mcp-rest-api/compare/v0.3.0...v0.4.0) (2025-01-08) 2 | 3 | 4 | ### Features 5 | 6 | * add custom header support ([9a48e0d](https://github.com/dkmaker/mcp-rest-api/commit/9a48e0d794a743f7a62c7cb73d6f5b1be9e44107)) 7 | 8 | ## [0.3.0](https://github.com/dkmaker/mcp-rest-api/compare/v0.2.0...v0.3.0) (2024-12-28) 9 | 10 | 11 | ### Features 12 | 13 | * add config documentation and improve URL resolution examples ([8c6100f](https://github.com/dkmaker/mcp-rest-api/commit/8c6100f47777605a0571edbd161ffd20fc48b640)) 14 | * add MCP resources for documentation ([a20cf35](https://github.com/dkmaker/mcp-rest-api/commit/a20cf352e9731841a8d7e833007a96bdd1a0c390)) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * correct response truncation to return first N bytes ([ce34649](https://github.com/dkmaker/mcp-rest-api/commit/ce34649c4d8e6bc6d740e8f3fbc6e3df517e0eec)) 20 | 21 | ## [0.2.0](https://github.com/dkmaker/mcp-rest-api/compare/0fdbe844dd4ce8b79f38a33df323a29e28253724...v0.2.0) (2024-12-21) 22 | 23 | 24 | ### Features 25 | 26 | * **ssl:** add SSL verification control with secure defaults ([0fdbe84](https://github.com/dkmaker/mcp-rest-api/commit/0fdbe844dd4ce8b79f38a33df323a29e28253724)) 27 | 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | # Use the Node.js image with the required version for the project 3 | FROM node:18-alpine AS builder 4 | 5 | # Set working directory 6 | WORKDIR /app 7 | 8 | # Copy package files to the working directory 9 | COPY package.json package-lock.json ./ 10 | 11 | # Install dependencies 12 | RUN npm install 13 | 14 | # Copy all files to the working directory 15 | COPY . . 16 | 17 | # Build the TypeScript files 18 | RUN npm run build 19 | 20 | # Create the final release image 21 | FROM node:18-alpine AS release 22 | 23 | # Set working directory 24 | WORKDIR /app 25 | 26 | # Copy built files and necessary package information 27 | COPY --from=builder /app/build /app/build 28 | COPY --from=builder /app/package.json /app/package-lock.json /app/node_modules ./ 29 | 30 | # Environment configuration for runtime (configured externally) 31 | ENV REST_BASE_URL="" 32 | ENV AUTH_BASIC_USERNAME="" 33 | ENV AUTH_BASIC_PASSWORD="" 34 | ENV AUTH_BEARER="" 35 | ENV AUTH_APIKEY_HEADER_NAME="" 36 | ENV AUTH_APIKEY_VALUE="" 37 | ENV REST_ENABLE_SSL_VERIFY="true" 38 | ENV REST_RESPONSE_SIZE_LIMIT="10000" 39 | 40 | # Command to run the server 41 | ENTRYPOINT ["node", "build/index.js"] -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dkmaker-mcp-rest-api", 3 | "version": "0.4.0", 4 | "description": "A generic REST API tester for testing HTTP endpoints", 5 | "license": "MIT", 6 | "type": "module", 7 | "bin": { 8 | "dkmaker-mcp-rest-api": "./build/index.js" 9 | }, 10 | "files": [ 11 | "build", 12 | "README.md", 13 | "LICENSE" 14 | ], 15 | "scripts": { 16 | "prebuild": "node scripts/build.js", 17 | "build": "tsc", 18 | "prepare": "npm run build", 19 | "watch": "tsc --watch", 20 | "inspector": "npx @modelcontextprotocol/inspector build/index.js" 21 | }, 22 | "dependencies": { 23 | "@modelcontextprotocol/sdk": "^1.0.4", 24 | "axios": "^1.7.9" 25 | }, 26 | "devDependencies": { 27 | "@commitlint/cli": "^19.6.1", 28 | "@commitlint/config-conventional": "^19.6.0", 29 | "@types/node": "^20.11.24", 30 | "typescript": "^5.3.3" 31 | }, 32 | "keywords": [ 33 | "mcp", 34 | "rest", 35 | "api", 36 | "http", 37 | "testing", 38 | "cline", 39 | "development", 40 | "typescript" 41 | ], 42 | "author": "zenturacp", 43 | "repository": { 44 | "type": "git", 45 | "url": "git+https://github.com/dkmaker/mcp-rest-api.git" 46 | }, 47 | "bugs": { 48 | "url": "https://github.com/dkmaker/mcp-rest-api/issues" 49 | }, 50 | "homepage": "https://github.com/dkmaker/mcp-rest-api#readme", 51 | "engines": { 52 | "node": ">=18.0.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from 'fs/promises'; 3 | import path from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | 9 | async function main() { 10 | const pkg = JSON.parse( 11 | await fs.readFile( 12 | path.join(__dirname, '..', 'package.json'), 13 | 'utf8' 14 | ) 15 | ); 16 | 17 | const versionPath = path.join(__dirname, '..', 'src', 'version.ts'); 18 | 19 | // Always generate version.ts with actual values during build 20 | const content = `// Auto-generated by build script 21 | export const VERSION = '${pkg.version}'; 22 | export const PACKAGE_NAME = '${pkg.name}'; 23 | export const SERVER_NAME = '${pkg.name.split('-').slice(-2).join('-')}'; 24 | `; 25 | 26 | await fs.writeFile(versionPath, content); 27 | console.log('Generated version.ts with package values'); 28 | 29 | // Copy resources to build directory 30 | const resourcesSrcDir = path.join(__dirname, '..', 'src', 'resources'); 31 | const resourcesBuildDir = path.join(__dirname, '..', 'build', 'resources'); 32 | 33 | try { 34 | await fs.mkdir(resourcesBuildDir, { recursive: true }); 35 | const files = await fs.readdir(resourcesSrcDir); 36 | 37 | for (const file of files) { 38 | await fs.copyFile( 39 | path.join(resourcesSrcDir, file), 40 | path.join(resourcesBuildDir, file) 41 | ); 42 | } 43 | console.log('Copied resources to build directory'); 44 | } catch (error) { 45 | console.error('Error copying resources:', error); 46 | throw error; 47 | } 48 | } 49 | 50 | main().catch(console.error); 51 | -------------------------------------------------------------------------------- /src/resources/response-format.md: -------------------------------------------------------------------------------- 1 | # Response Format Documentation 2 | 3 | The REST API testing tool returns a comprehensive JSON response containing request details, response information, and validation results. 4 | 5 | ## Response Structure 6 | 7 | ```json 8 | { 9 | "request": { 10 | "url": "http://api.example.com/users", 11 | "method": "GET", 12 | "headers": {}, 13 | "body": null, 14 | "authMethod": "none" 15 | }, 16 | "response": { 17 | "statusCode": 200, 18 | "statusText": "OK", 19 | "timing": "123ms", 20 | "headers": {}, 21 | "body": {} 22 | }, 23 | "validation": { 24 | "isError": false, 25 | "messages": ["Request completed successfully"] 26 | } 27 | } 28 | ``` 29 | 30 | ## Response Fields 31 | 32 | ### Request Details 33 | - `url`: Full URL including base URL and endpoint 34 | - `method`: HTTP method used 35 | - `headers`: Request headers sent 36 | - `body`: Request body (if applicable) 37 | - `authMethod`: Authentication method used (none, basic, bearer, or apikey) 38 | 39 | ### Response Details 40 | - `statusCode`: HTTP status code 41 | - `statusText`: Status message 42 | - `timing`: Request duration in milliseconds 43 | - `headers`: Response headers received 44 | - `body`: Response body content 45 | 46 | ### Validation 47 | - `isError`: true if status code >= 400 48 | - `messages`: Array of validation or error messages 49 | 50 | ## Error Response Example 51 | 52 | ```json 53 | { 54 | "error": { 55 | "message": "Connection refused", 56 | "code": "ECONNREFUSED", 57 | "request": { 58 | "url": "http://api.example.com/users", 59 | "method": "GET", 60 | "headers": {}, 61 | "body": null 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: 9 | - restBaseUrl 10 | properties: 11 | restBaseUrl: 12 | type: string 13 | description: The base URL for the REST API. 14 | authBasicUsername: 15 | type: string 16 | description: The username for Basic Authentication. 17 | authBasicPassword: 18 | type: string 19 | description: The password for Basic Authentication. 20 | authBearer: 21 | type: string 22 | description: The bearer token for authentication. 23 | authApiKeyHeaderName: 24 | type: string 25 | description: The header name for API Key Authentication. 26 | authApiKeyValue: 27 | type: string 28 | description: The API key value for API Key Authentication. 29 | restEnableSslVerify: 30 | type: boolean 31 | default: true 32 | description: Enable or disable SSL verification. 33 | restResponseSizeLimit: 34 | type: number 35 | default: 10000 36 | description: The maximum response size limit in bytes. 37 | commandFunction: 38 | # A function that produces the CLI command to start the MCP on stdio. 39 | |- 40 | config => ({ command: 'node', args: ['build/index.js'], env: { REST_BASE_URL: config.restBaseUrl, AUTH_BASIC_USERNAME: config.authBasicUsername, AUTH_BASIC_PASSWORD: config.authBasicPassword, AUTH_BEARER: config.authBearer, AUTH_APIKEY_HEADER_NAME: config.authApiKeyHeaderName, AUTH_APIKEY_VALUE: config.authApiKeyValue, REST_ENABLE_SSL_VERIFY: config.restEnableSslVerify.toString(), REST_RESPONSE_SIZE_LIMIT: config.restResponseSizeLimit.toString() } }) -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | jobs: 13 | publish: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: '18.x' 24 | registry-url: 'https://registry.npmjs.org' 25 | 26 | - name: Install dependencies 27 | run: npm ci 28 | 29 | - name: Conventional Changelog Action 30 | id: changelog 31 | uses: TriPSs/conventional-changelog-action@v6 32 | with: 33 | github-token: ${{ secrets.GITHUB_TOKEN }} 34 | git-message: 'chore(release): {version}' 35 | config-file-path: '.github/conventional-changelog.config.cjs' 36 | tag-prefix: 'v' 37 | output-file: 'CHANGELOG.md' 38 | skip-version-file: false 39 | skip-commit: false 40 | skip-on-empty: false 41 | git-user-name: ${{ secrets.CHANGELOG_GIT_NAME }} 42 | git-user-email: ${{ secrets.CHANGELOG_GIT_EMAIL }} 43 | 44 | - name: Build 45 | run: npm run build 46 | 47 | - name: Create Release 48 | if: steps.changelog.outputs.skipped == 'false' 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | run: | 52 | gh release create v${{ steps.changelog.outputs.version }} \ 53 | --title "Release v${{ steps.changelog.outputs.version }}" \ 54 | --notes "${{ steps.changelog.outputs.clean_changelog }}" 55 | 56 | - name: Publish to NPM 57 | if: steps.changelog.outputs.skipped == 'false' 58 | run: npm publish 59 | env: 60 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 61 | -------------------------------------------------------------------------------- /src/resources/config.md: -------------------------------------------------------------------------------- 1 | # REST API Tester Configuration 2 | 3 | This document describes all available configuration options for the REST API testing tool. 4 | 5 | ## Core Configuration 6 | 7 | ### REST_BASE_URL (Required) 8 | - Description: The base URL that all endpoint paths will be resolved against 9 | - Example: `http://localhost:3000` or `https://api.example.com` 10 | - Usage: All endpoint paths will be appended to this URL. For example, if REST_BASE_URL is `http://localhost:3000` and you use the endpoint `/api/users`, the full URL will be `http://localhost:3000/api/users` 11 | 12 | ### REST_RESPONSE_SIZE_LIMIT (Optional) 13 | - Description: Maximum size in bytes for response data 14 | - Default: 10000 (10KB) 15 | - Example: `50000` for 50KB limit 16 | - Usage: Helps prevent memory issues with large responses. If a response exceeds this size, it will be truncated and a warning message will be included 17 | 18 | ### REST_ENABLE_SSL_VERIFY (Optional) 19 | - Description: Controls SSL certificate verification 20 | - Default: `true` 21 | - Values: Set to `false` to disable SSL verification for self-signed certificates 22 | - Usage: Disable when testing APIs with self-signed certificates in development environments 23 | 24 | ## Custom Headers Configuration 25 | 26 | ### Custom Headers (Optional) 27 | - Description: Add custom headers to all requests using environment variables 28 | - Pattern: `HEADER_=` (prefix is case-insensitive) 29 | - Examples: 30 | ```bash 31 | HEADER_X-API-Version=2.0 32 | header_Custom-Client=my-client 33 | HeAdEr_Accept=application/json 34 | ``` 35 | - Usage: Headers are added to all requests. The header name after `HEADER_` preserves its exact case 36 | - Priority: Per-request headers > Authentication headers > Custom global headers 37 | 38 | ## Authentication Configuration 39 | 40 | The tool supports three authentication methods. Configure one based on your API's requirements. 41 | 42 | ### Basic Authentication 43 | - REST_BASIC_USERNAME: Username for Basic Auth 44 | - REST_BASIC_PASSWORD: Password for Basic Auth 45 | - Usage: When both are set, requests will include Basic Auth header 46 | 47 | ### Bearer Token 48 | - REST_BEARER: Bearer token value 49 | - Usage: When set, requests will include `Authorization: Bearer ` header 50 | 51 | ### API Key 52 | - REST_APIKEY_HEADER_NAME: Name of the header for API key 53 | - REST_APIKEY_VALUE: Value of the API key 54 | - Example: 55 | ``` 56 | REST_APIKEY_HEADER_NAME=X-API-Key 57 | REST_APIKEY_VALUE=your-api-key-here 58 | ``` 59 | - Usage: When both are set, requests will include the specified header with the API key 60 | 61 | ## Configuration Examples 62 | 63 | ### Local Development 64 | ```bash 65 | REST_BASE_URL=http://localhost:3000 66 | REST_ENABLE_SSL_VERIFY=false 67 | REST_RESPONSE_SIZE_LIMIT=50000 68 | ``` 69 | 70 | ### Production API with Bearer Token 71 | ```bash 72 | REST_BASE_URL=https://api.example.com 73 | REST_BEARER=your-bearer-token 74 | ``` 75 | 76 | ### API with Basic Auth 77 | ```bash 78 | REST_BASE_URL=https://api.example.com 79 | REST_BASIC_USERNAME=admin 80 | REST_BASIC_PASSWORD=secret 81 | ``` 82 | 83 | ### API with API Key 84 | ```bash 85 | REST_BASE_URL=https://api.example.com 86 | REST_APIKEY_HEADER_NAME=X-API-Key 87 | REST_APIKEY_VALUE=your-api-key 88 | ``` 89 | 90 | ### API with Custom Headers 91 | ```bash 92 | REST_BASE_URL=https://api.example.com 93 | HEADER_X-API-Version=2.0 94 | HEADER_Custom-Client=my-client 95 | HEADER_Accept=application/json 96 | ``` 97 | 98 | ## Changing Configuration 99 | 100 | Configuration can be updated by: 101 | 1. Setting environment variables before starting the server 102 | 2. Modifying the MCP server configuration file 103 | 3. Using environment variable commands in your terminal 104 | 105 | Remember to restart the server after changing configuration for the changes to take effect. 106 | -------------------------------------------------------------------------------- /src/resources/examples.md: -------------------------------------------------------------------------------- 1 | # REST API Testing Examples 2 | 3 | ⚠️ IMPORTANT: 4 | - Only provide the endpoint path in the `endpoint` argument—do not include full URLs. Your path will be automatically resolved to the full URL using the configured base URL or the optional `host` argument. 5 | - To override the base URL for a single request, use the optional `host` argument. This must be a valid URL starting with `http://` or `https://`, and may include a path (trailing slashes will be removed). 6 | 7 | For example, if the base URL is `http://localhost:3000`: 8 | ✅ Correct: `"/api/users"` → Resolves to: `http://localhost:3000/api/users` 9 | ❌ Incorrect: `"http://localhost:3000/api/users"` or `"www.example.com/api/users"` 10 | 11 | If you use a `host` argument: 12 | ✅ Correct: `"host": "https://api.example.com/v1", "endpoint": "/users"` → Resolves to: `https://api.example.com/v1/users` 13 | 14 | ## Basic GET Request 15 | ```typescript 16 | use_mcp_tool('rest-api', 'test_request', { 17 | "method": "GET", 18 | "endpoint": "/users" // Will be appended to REST_BASE_URL or 'host' if provided 19 | }); 20 | ``` 21 | 22 | ## GET with Query Parameters 23 | ```typescript 24 | use_mcp_tool('rest-api', 'test_request', { 25 | "method": "GET", 26 | "endpoint": "/users?role=admin&status=active" // Always a path, not a full URL 27 | }); 28 | ``` 29 | 30 | ## POST Request with Body 31 | ```typescript 32 | use_mcp_tool('rest-api', 'test_request', { 33 | "method": "POST", 34 | "endpoint": "/users", 35 | "body": { 36 | "name": "John Doe", 37 | "email": "john@example.com" 38 | } 39 | }); 40 | ``` 41 | 42 | ## Request with Custom Headers 43 | ```typescript 44 | use_mcp_tool('rest-api', 'test_request', { 45 | "method": "GET", 46 | "endpoint": "/secure-resource", 47 | "headers": { 48 | "Custom-Header": "value", 49 | "Another-Header": "another-value" 50 | } 51 | }); 52 | ``` 53 | 54 | ## PUT Request Example 55 | ```typescript 56 | use_mcp_tool('rest-api', 'test_request', { 57 | "method": "PUT", 58 | "endpoint": "/users/123", 59 | "body": { 60 | "name": "Updated Name", 61 | "status": "inactive" 62 | } 63 | }); 64 | ``` 65 | 66 | ## DELETE Request Example 67 | ```typescript 68 | use_mcp_tool('rest-api', 'test_request', { 69 | "method": "DELETE", 70 | "endpoint": "/users/123" 71 | }); 72 | ``` 73 | 74 | ## PATCH Request Example 75 | ```typescript 76 | use_mcp_tool('rest-api', 'test_request', { 77 | "method": "PATCH", 78 | "endpoint": "/users/123", 79 | "body": { 80 | "status": "active" 81 | } 82 | }); 83 | ``` 84 | 85 | ## Using the Optional `host` Argument 86 | You can override the default base URL for a single request by providing a `host` argument. This must be a valid URL starting with `http://` or `https://`, and may include a path (trailing slashes will be removed). 87 | 88 | ```typescript 89 | use_mcp_tool('rest-api', 'test_request', { 90 | "method": "GET", 91 | "endpoint": "/users", 92 | "host": "https://api.example.com/v1" // The request will go to https://api.example.com/v1/users 93 | }); 94 | ``` 95 | 96 | - The `host` argument must include the protocol (http or https). 97 | - If a path is included, any trailing slash will be removed. 98 | - If `host` is invalid, you will receive a clear error message. 99 | - The `endpoint` argument must always be a path, never a full URL. 100 | 101 | ## Changing Base URL 102 | If you need to test against a different base URL for all requests, update the base URL configuration rather than including the full URL in the endpoint parameter. For a single request, use the `host` argument as shown above. 103 | 104 | Example: 105 | ```bash 106 | # Instead of this: 107 | ❌ "endpoint": "https://api.example.com/users" # Wrong - don't include the full URL 108 | 109 | # Do this: 110 | # 1. Update the base URL configuration to: https://api.example.com 111 | # 2. Then use just the path: 112 | ✅ "endpoint": "/users" # This will resolve to: https://api.example.com/users 113 | # Or, for a single request: 114 | ✅ "host": "https://api.example.com", "endpoint": "/users" # This will resolve to: https://api.example.com/users 115 | ``` 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/dkmaker-mcp-rest-api-badge.png)](https://mseep.ai/app/dkmaker-mcp-rest-api) 2 | 3 | # MCP REST API Tester 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 5 | [![NPM Package](https://img.shields.io/npm/v/dkmaker-mcp-rest-api.svg)](https://www.npmjs.com/package/dkmaker-mcp-rest-api) 6 | [![smithery badge](https://smithery.ai/badge/dkmaker-mcp-rest-api)](https://smithery.ai/server/dkmaker-mcp-rest-api) 7 | 8 | A TypeScript-based MCP server that enables testing of REST APIs through Cline. This tool allows you to test and interact with any REST API endpoints directly from your development environment. 9 | 10 | 11 | 12 | 13 | 14 | ## Installation 15 | 16 | ### Installing via Smithery 17 | 18 | To install REST API Tester for Claude Desktop automatically via [Smithery](https://smithery.ai/server/dkmaker-mcp-rest-api): 19 | 20 | ```bash 21 | npx -y @smithery/cli install dkmaker-mcp-rest-api --client claude 22 | ``` 23 | 24 | ### Installing Manually 25 | 1. Install the package globally: 26 | ```bash 27 | npm install -g dkmaker-mcp-rest-api 28 | ``` 29 | 30 | 2. Configure Cline Custom Instructions: 31 | 32 | To ensure Cline understands how to effectively use this tool, add the following to your Cline custom instructions (Settings > Custom Instructions): 33 | 34 | ```markdown 35 | # REST API Testing Instructions 36 | 37 | The `test_request` tool enables testing, debugging, and interacting with REST API endpoints. The tool provides comprehensive request/response information and handles authentication automatically. 38 | 39 | ## When to Use 40 | 41 | - Testing specific API endpoints 42 | - Debugging API responses 43 | - Verifying API functionality 44 | - Checking response times 45 | - Validating request/response formats 46 | - Testing local development servers 47 | - Testing API sequences 48 | - Verifying error handling 49 | 50 | ## Key Features 51 | 52 | - Supports GET, POST, PUT, DELETE, PATCH methods 53 | - Handles authentication (Basic, Bearer, API Key) 54 | - Normalizes endpoints automatically 55 | - Provides detailed response information 56 | - Configurable SSL verification and response limits 57 | 58 | ## Resources 59 | 60 | The following resources provide detailed documentation: 61 | 62 | - examples: Usage examples and common patterns 63 | - response-format: Response structure and fields 64 | - config: Configuration options and setup guide 65 | 66 | Access these resources to understand usage, response formats, and configuration options. 67 | 68 | ## Important Notes 69 | 70 | - Review API implementation for expected behavior 71 | - Handle sensitive data appropriately 72 | - Consider rate limits and API constraints 73 | - Restart server after configuration changes 74 | ``` 75 | 76 | 3. Add the server to your MCP configuration: 77 | 78 | While these instructions are for Cline, the server should work with any MCP implementation. Configure based on your operating system: 79 | 80 | ### Windows 81 | ⚠️ **IMPORTANT**: Due to a known issue with Windows path resolution ([issue #40](https://github.com/modelcontextprotocol/servers/issues/40)), you must use the full path instead of %APPDATA%. 82 | 83 | Add to `C:\Users\\AppData\Roaming\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json`: 84 | ```json 85 | { 86 | "mcpServers": { 87 | "rest-api": { 88 | "command": "node", 89 | "args": [ 90 | "C:/Users//AppData/Roaming/npm/node_modules/dkmaker-mcp-rest-api/build/index.js" 91 | ], 92 | "env": { 93 | "REST_BASE_URL": "https://api.example.com", 94 | // Basic Auth 95 | "AUTH_BASIC_USERNAME": "your-username", 96 | "AUTH_BASIC_PASSWORD": "your-password", 97 | // OR Bearer Token 98 | "AUTH_BEARER": "your-token", 99 | // OR API Key 100 | "AUTH_APIKEY_HEADER_NAME": "X-API-Key", 101 | "AUTH_APIKEY_VALUE": "your-api-key", 102 | // SSL Verification (enabled by default) 103 | "REST_ENABLE_SSL_VERIFY": "false", // Set to false to disable SSL verification for self-signed certificates 104 | // Response Size Limit (optional, defaults to 10000 bytes) 105 | "REST_RESPONSE_SIZE_LIMIT": "10000", // Maximum response size in bytes 106 | // Custom Headers (optional) 107 | "HEADER_X-API-Version": "2.0", 108 | "HEADER_Custom-Client": "my-client", 109 | "HEADER_Accept": "application/json" 110 | } 111 | } 112 | } 113 | } 114 | ``` 115 | 116 | ### macOS 117 | Add to `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`: 118 | ```json 119 | { 120 | "mcpServers": { 121 | "rest-api": { 122 | "command": "npx", 123 | "args": [ 124 | "-y", 125 | "dkmaker-mcp-rest-api" 126 | ], 127 | "env": { 128 | "REST_BASE_URL": "https://api.example.com", 129 | // Basic Auth 130 | "AUTH_BASIC_USERNAME": "your-username", 131 | "AUTH_BASIC_PASSWORD": "your-password", 132 | // OR Bearer Token 133 | "AUTH_BEARER": "your-token", 134 | // OR API Key 135 | "AUTH_APIKEY_HEADER_NAME": "X-API-Key", 136 | "AUTH_APIKEY_VALUE": "your-api-key", 137 | // SSL Verification (enabled by default) 138 | "REST_ENABLE_SSL_VERIFY": "false", // Set to false to disable SSL verification for self-signed certificates 139 | // Custom Headers (optional) 140 | "HEADER_X-API-Version": "2.0", 141 | "HEADER_Custom-Client": "my-client", 142 | "HEADER_Accept": "application/json" 143 | } 144 | } 145 | } 146 | } 147 | ``` 148 | 149 | Note: Replace the environment variables with your actual values. Only configure one authentication method at a time: 150 | 1. Basic Authentication (username/password) 151 | 2. Bearer Token (if Basic Auth is not configured) 152 | 3. API Key (if neither Basic Auth nor Bearer Token is configured) 153 | 154 | ## Features 155 | 156 | - Test REST API endpoints with different HTTP methods 157 | - Support for GET, POST, PUT, DELETE, and PATCH requests 158 | - Detailed response information including status, headers, and body 159 | - Custom Headers: 160 | - Global headers via HEADER_* environment variables 161 | - Case-insensitive prefix (HEADER_, header_, HeAdEr_) 162 | - Case preservation for header names 163 | - Priority-based application (per-request > auth > custom) 164 | - Request body handling for POST/PUT methods 165 | - Response Size Management: 166 | - Automatic response size limiting (default: 10KB/10000 bytes) 167 | - Configurable size limit via REST_RESPONSE_SIZE_LIMIT environment variable 168 | - Clear truncation metadata when responses exceed limit 169 | - Preserves response structure while only truncating body content 170 | 171 | - SSL Certificate Verification: 172 | - Enabled by default for secure operation 173 | - Can be disabled for self-signed certificates or development environments 174 | - Control via REST_ENABLE_SSL_VERIFY environment variable 175 | - Multiple authentication methods: 176 | - Basic Authentication (username/password) 177 | - Bearer Token Authentication 178 | - API Key Authentication (custom header) 179 | 180 | ## Usage Examples 181 | 182 | Once installed and configured, you can use the REST API Tester through Cline to test your API endpoints: 183 | 184 | ```typescript 185 | // Test a GET endpoint 186 | use_mcp_tool('rest-api', 'test_request', { 187 | "method": "GET", 188 | "endpoint": "/users" 189 | }); 190 | 191 | // Test a POST endpoint with body 192 | use_mcp_tool('rest-api', 'test_request', { 193 | "method": "POST", 194 | "endpoint": "/users", 195 | "body": { 196 | "name": "John Doe", 197 | "email": "john@example.com" 198 | } 199 | }); 200 | 201 | // Test with custom headers 202 | use_mcp_tool('rest-api', 'test_request', { 203 | "method": "GET", 204 | "endpoint": "/products", 205 | "headers": { 206 | "Accept-Language": "en-US", 207 | "X-Custom-Header": "custom-value" 208 | } 209 | }); 210 | ``` 211 | 212 | ## Development 213 | 214 | 1. Clone the repository: 215 | ```bash 216 | git clone https://github.com/zenturacp/mcp-rest-api.git 217 | cd mcp-rest-api 218 | ``` 219 | 220 | 2. Install dependencies: 221 | ```bash 222 | npm install 223 | ``` 224 | 225 | 3. Build the project: 226 | ```bash 227 | npm run build 228 | ``` 229 | 230 | For development with auto-rebuild: 231 | ```bash 232 | npm run watch 233 | ``` 234 | 235 | ## License 236 | 237 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 238 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 4 | import { 5 | CallToolRequestSchema, 6 | ErrorCode, 7 | ListToolsRequestSchema, 8 | ListResourcesRequestSchema, 9 | ReadResourceRequestSchema, 10 | McpError, 11 | } from '@modelcontextprotocol/sdk/types.js'; 12 | import axios, { AxiosInstance, AxiosRequestConfig, Method } from 'axios'; 13 | import { VERSION, SERVER_NAME } from './version.js'; 14 | 15 | if (!process.env.REST_BASE_URL) { 16 | throw new Error('REST_BASE_URL environment variable is required'); 17 | } 18 | 19 | // Default response size limit: 10KB (10000 bytes) 20 | const RESPONSE_SIZE_LIMIT = process.env.REST_RESPONSE_SIZE_LIMIT 21 | ? parseInt(process.env.REST_RESPONSE_SIZE_LIMIT, 10) 22 | : 10000; 23 | 24 | if (isNaN(RESPONSE_SIZE_LIMIT) || RESPONSE_SIZE_LIMIT <= 0) { 25 | throw new Error('REST_RESPONSE_SIZE_LIMIT must be a positive number'); 26 | } 27 | const AUTH_BASIC_USERNAME = process.env.AUTH_BASIC_USERNAME; 28 | const AUTH_BASIC_PASSWORD = process.env.AUTH_BASIC_PASSWORD; 29 | const AUTH_BEARER = process.env.AUTH_BEARER; 30 | const AUTH_APIKEY_HEADER_NAME = process.env.AUTH_APIKEY_HEADER_NAME; 31 | const AUTH_APIKEY_VALUE = process.env.AUTH_APIKEY_VALUE; 32 | const REST_ENABLE_SSL_VERIFY = process.env.REST_ENABLE_SSL_VERIFY !== 'false'; 33 | 34 | interface EndpointArgs { 35 | method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; 36 | endpoint: string; 37 | body?: any; 38 | headers?: Record; 39 | host?: string; 40 | } 41 | 42 | interface ValidationResult { 43 | isError: boolean; 44 | messages: string[]; 45 | truncated?: { 46 | originalSize: number; 47 | returnedSize: number; 48 | truncationPoint: number; 49 | sizeLimit: number; 50 | }; 51 | } 52 | 53 | // Function to sanitize headers by removing sensitive values and non-approved headers 54 | const sanitizeHeaders = ( 55 | headers: Record, 56 | isFromOptionalParams: boolean = false 57 | ): Record => { 58 | const sanitized: Record = {}; 59 | 60 | for (const [key, value] of Object.entries(headers)) { 61 | const lowerKey = key.toLowerCase(); 62 | 63 | // Always include headers from optional parameters 64 | if (isFromOptionalParams) { 65 | sanitized[key] = value; 66 | continue; 67 | } 68 | 69 | // Handle authentication headers 70 | if ( 71 | lowerKey === 'authorization' || 72 | (AUTH_APIKEY_HEADER_NAME && lowerKey === AUTH_APIKEY_HEADER_NAME.toLowerCase()) 73 | ) { 74 | sanitized[key] = '[REDACTED]'; 75 | continue; 76 | } 77 | 78 | // For headers from config/env 79 | const customHeaders = getCustomHeaders(); 80 | if (key in customHeaders) { 81 | // Show value only for headers that are in the approved list 82 | const safeHeaders = new Set([ 83 | 'accept', 84 | 'accept-language', 85 | 'content-type', 86 | 'user-agent', 87 | 'cache-control', 88 | 'if-match', 89 | 'if-none-match', 90 | 'if-modified-since', 91 | 'if-unmodified-since' 92 | ]); 93 | const lowerKey = key.toLowerCase(); 94 | sanitized[key] = safeHeaders.has(lowerKey) ? value : '[REDACTED]'; 95 | } 96 | } 97 | 98 | return sanitized; 99 | }; 100 | 101 | interface ResponseObject { 102 | request: { 103 | url: string; 104 | method: string; 105 | headers: Record; 106 | body: any; 107 | authMethod: string; 108 | }; 109 | response: { 110 | statusCode: number; 111 | statusText: string; 112 | timing: string; 113 | headers: Record; 114 | body: any; 115 | }; 116 | validation: ValidationResult; 117 | } 118 | 119 | const normalizeBaseUrl = (url: string): string => url.replace(/\/+$/, ''); 120 | 121 | const isValidEndpointArgs = (args: any): args is EndpointArgs => { 122 | if (typeof args !== 'object' || args === null) return false; 123 | if (!['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(args.method)) return false; 124 | if (typeof args.endpoint !== 'string') return false; 125 | if (args.headers !== undefined && (typeof args.headers !== 'object' || args.headers === null)) return false; 126 | 127 | // Check if endpoint contains a full URL 128 | const urlPattern = /^(https?:\/\/|www\.)/i; 129 | if (urlPattern.test(args.endpoint)) { 130 | throw new McpError( 131 | ErrorCode.InvalidParams, 132 | `Invalid endpoint format. Do not include full URLs. Instead of "${args.endpoint}", use just the path (e.g. "/api/users"). ` + 133 | `Your path will be resolved to: ${process.env.REST_BASE_URL}${args.endpoint.replace(/^\/+|\/+$/g, '')}. ` + 134 | `To test a different base URL, update the REST_BASE_URL environment variable.` 135 | ); 136 | } 137 | // Validate .host if present 138 | if (args.host !== undefined) { 139 | try { 140 | const url = new URL(args.host); 141 | if (!/^https?:$/.test(url.protocol)) { 142 | throw new Error(); 143 | } 144 | // Remove trailing slash if present 145 | if (url.pathname.endsWith('/') && url.pathname !== '/') { 146 | url.pathname = url.pathname.replace(/\/+$/, ''); 147 | args.host = url.origin + url.pathname; 148 | } else { 149 | args.host = url.origin + url.pathname; 150 | } 151 | } catch (e) { 152 | throw new McpError(ErrorCode.InvalidParams, `Invalid host format. The 'host' argument must be a valid URL starting with http:// or https://, e.g. "https://example.com" or "http://localhost:3001/api/v1". Received: "${args.host}"`); 153 | } 154 | } 155 | 156 | return true; 157 | }; 158 | 159 | const hasBasicAuth = () => AUTH_BASIC_USERNAME && AUTH_BASIC_PASSWORD; 160 | const hasBearerAuth = () => !!AUTH_BEARER; 161 | const hasApiKeyAuth = () => AUTH_APIKEY_HEADER_NAME && AUTH_APIKEY_VALUE; 162 | 163 | // Collect custom headers from environment variables 164 | const getCustomHeaders = (): Record => { 165 | const headers: Record = {}; 166 | const headerPrefix = /^header_/i; // Case-insensitive match for 'header_' 167 | 168 | for (const [key, value] of Object.entries(process.env)) { 169 | if (headerPrefix.test(key) && value !== undefined) { 170 | // Extract header name after the prefix, preserving case 171 | const headerName = key.replace(headerPrefix, ''); 172 | headers[headerName] = value; 173 | } 174 | } 175 | 176 | return headers; 177 | }; 178 | 179 | class RestTester { 180 | private server!: Server; 181 | private axiosInstance!: AxiosInstance; 182 | 183 | constructor() { 184 | this.setupServer(); 185 | } 186 | 187 | private async setupServer() { 188 | this.server = new Server( 189 | { 190 | name: SERVER_NAME, 191 | version: VERSION, 192 | }, 193 | { 194 | capabilities: { 195 | tools: {}, 196 | resources: {}, 197 | }, 198 | } 199 | ); 200 | 201 | const https = await import('https'); 202 | this.axiosInstance = axios.create({ 203 | baseURL: normalizeBaseUrl(process.env.REST_BASE_URL!), 204 | validateStatus: () => true, // Allow any status code 205 | httpsAgent: REST_ENABLE_SSL_VERIFY ? undefined : new https.Agent({ // Disable SSL verification only when explicitly set to false 206 | rejectUnauthorized: false 207 | }) 208 | }); 209 | 210 | this.setupToolHandlers(); 211 | this.setupResourceHandlers(); 212 | 213 | this.server.onerror = (error) => console.error('[MCP Error]', error); 214 | process.on('SIGINT', async () => { 215 | await this.server.close(); 216 | process.exit(0); 217 | }); 218 | } 219 | 220 | private setupResourceHandlers() { 221 | this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ 222 | resources: [ 223 | { 224 | uri: `${SERVER_NAME}://examples`, 225 | name: 'REST API Usage Examples', 226 | description: 'Detailed examples of using the REST API testing tool', 227 | mimeType: 'text/markdown' 228 | }, 229 | { 230 | uri: `${SERVER_NAME}://response-format`, 231 | name: 'Response Format Documentation', 232 | description: 'Documentation of the response format and structure', 233 | mimeType: 'text/markdown' 234 | }, 235 | { 236 | uri: `${SERVER_NAME}://config`, 237 | name: 'Configuration Documentation', 238 | description: 'Documentation of all configuration options and how to use them', 239 | mimeType: 'text/markdown' 240 | } 241 | ] 242 | })); 243 | 244 | this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { 245 | const uriPattern = new RegExp(`^${SERVER_NAME}://(.+)$`); 246 | const match = request.params.uri.match(uriPattern); 247 | 248 | if (!match) { 249 | throw new McpError( 250 | ErrorCode.InvalidRequest, 251 | `Invalid resource URI format: ${request.params.uri}` 252 | ); 253 | } 254 | 255 | const resource = match[1]; 256 | const fs = await import('fs'); 257 | const path = await import('path'); 258 | 259 | try { 260 | const url = await import('url'); 261 | const __filename = url.fileURLToPath(import.meta.url); 262 | const __dirname = path.dirname(__filename); 263 | 264 | // In the built app, resources are in build/resources 265 | // In development, they're in src/resources 266 | const resourcePath = path.join(__dirname, 'resources', `${resource}.md`); 267 | const content = await fs.promises.readFile(resourcePath, 'utf8'); 268 | 269 | return { 270 | contents: [{ 271 | uri: request.params.uri, 272 | mimeType: 'text/markdown', 273 | text: content 274 | }] 275 | }; 276 | } catch (error) { 277 | throw new McpError( 278 | ErrorCode.InvalidRequest, 279 | `Resource not found: ${resource}` 280 | ); 281 | } 282 | }); 283 | } 284 | 285 | private setupToolHandlers() { 286 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ 287 | tools: [ 288 | { 289 | name: 'test_request', 290 | description: `Test a REST API endpoint and get detailed response information. Base URL: ${normalizeBaseUrl(process.env.REST_BASE_URL!)} | SSL Verification ${REST_ENABLE_SSL_VERIFY ? 'enabled' : 'disabled'} (see config resource for SSL settings) | Authentication: ${ 291 | hasBasicAuth() ? 292 | `Basic Auth with username: ${AUTH_BASIC_USERNAME}` : 293 | hasBearerAuth() ? 294 | 'Bearer token authentication configured' : 295 | hasApiKeyAuth() ? 296 | `API Key using header: ${AUTH_APIKEY_HEADER_NAME}` : 297 | 'No authentication configured' 298 | } | ${(() => { 299 | const customHeaders = getCustomHeaders(); 300 | if (Object.keys(customHeaders).length === 0) { 301 | return 'No custom headers defined (see config resource for headers)'; 302 | } 303 | 304 | // List of common headers that are safe to show values for 305 | const safeHeaders = new Set([ 306 | 'accept', 307 | 'accept-language', 308 | 'content-type', 309 | 'user-agent', 310 | 'cache-control', 311 | 'if-match', 312 | 'if-none-match', 313 | 'if-modified-since', 314 | 'if-unmodified-since' 315 | ]); 316 | 317 | const headerList = Object.entries(customHeaders).map(([name, value]) => { 318 | const lowerName = name.toLowerCase(); 319 | return safeHeaders.has(lowerName) ? 320 | `${name}(${value})` : 321 | name; 322 | }).join(', '); 323 | 324 | return `Custom headers defined: ${headerList} (see config resource for headers)`; 325 | })()} | The tool automatically: - Normalizes endpoints (adds leading slash, removes trailing slashes) - Handles authentication header injection - Applies custom headers from HEADER_* environment variables - Accepts any HTTP status code as valid - Limits response size to ${RESPONSE_SIZE_LIMIT} bytes (see config resource for size limit settings) - Returns detailed response information including: * Full URL called * Status code and text * Response headers * Response body * Request details (method, headers, body) * Response timing * Validation messages | Error Handling: - Network errors are caught and returned with descriptive messages - Invalid status codes are still returned with full response details - Authentication errors include the attempted auth method | See the config resource for all configuration options, including header configuration. 326 | `, 327 | inputSchema: { 328 | type: 'object', 329 | properties: { 330 | method: { 331 | type: 'string', 332 | enum: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], 333 | description: 'HTTP method to use', 334 | }, 335 | endpoint: { 336 | type: 'string', 337 | description: `Endpoint path (e.g. "/users"). Do not include full URLs - only the path. Example: "/api/users" will resolve to "${normalizeBaseUrl(process.env.REST_BASE_URL!)}/api/users"`, 338 | }, 339 | body: { 340 | type: 'object', 341 | description: 'Optional request body for POST/PUT requests', 342 | }, 343 | headers: { 344 | type: 'object', 345 | description: 'Optional request headers for one-time use. IMPORTANT: Do not use for sensitive data like API keys - those should be configured via environment variables. This parameter is intended for dynamic, non-sensitive headers that may be needed for specific requests.', 346 | additionalProperties: { 347 | type: 'string' 348 | } 349 | } 350 | }, 351 | required: ['method', 'endpoint'], 352 | }, 353 | }, 354 | ], 355 | })); 356 | 357 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 358 | if (request.params.name !== 'test_request') { 359 | throw new McpError( 360 | ErrorCode.MethodNotFound, 361 | `Unknown tool: ${request.params.name}` 362 | ); 363 | } 364 | 365 | if (!isValidEndpointArgs(request.params.arguments)) { 366 | throw new McpError( 367 | ErrorCode.InvalidParams, 368 | 'Invalid test endpoint arguments' 369 | ); 370 | } 371 | 372 | // Ensure endpoint starts with / and remove any trailing slashes 373 | const normalizedEndpoint = `/${request.params.arguments.endpoint.replace(/^\/+|\/+$/g, '')}`; 374 | 375 | const fullUrl = `${request.params.arguments.host || process.env.REST_BASE_URL}${normalizedEndpoint}`; 376 | // Initialize request config 377 | const config: AxiosRequestConfig = { 378 | method: request.params.arguments.method as Method, 379 | url: fullUrl, 380 | headers: {}, 381 | }; 382 | 383 | // Add request body for POST/PUT/PATCH 384 | if (['POST', 'PUT', 'PATCH'].includes(request.params.arguments.method) && request.params.arguments.body) { 385 | config.data = request.params.arguments.body; 386 | } 387 | 388 | // Apply headers in order of priority (lowest to highest) 389 | 390 | // 1. Custom global headers (lowest priority) 391 | const customHeaders = getCustomHeaders(); 392 | config.headers = { 393 | ...customHeaders, 394 | ...config.headers, 395 | ...(request.params.arguments.headers || {}) // Request-specific headers (middle priority) 396 | }; 397 | 398 | // 3. Authentication headers (highest priority) 399 | if (hasBasicAuth()) { 400 | const base64Credentials = Buffer.from(`${AUTH_BASIC_USERNAME}:${AUTH_BASIC_PASSWORD}`).toString('base64'); 401 | config.headers = { 402 | ...config.headers, 403 | 'Authorization': `Basic ${base64Credentials}` 404 | }; 405 | } else if (hasBearerAuth()) { 406 | config.headers = { 407 | ...config.headers, 408 | 'Authorization': `Bearer ${AUTH_BEARER}` 409 | }; 410 | } else if (hasApiKeyAuth()) { 411 | config.headers = { 412 | ...config.headers, 413 | [AUTH_APIKEY_HEADER_NAME as string]: AUTH_APIKEY_VALUE 414 | }; 415 | } 416 | 417 | try { 418 | const startTime = Date.now(); 419 | const response = await this.axiosInstance.request(config); 420 | const endTime = Date.now(); 421 | 422 | // Determine auth method used 423 | let authMethod = 'none'; 424 | if (hasBasicAuth()) authMethod = 'basic'; 425 | else if (hasBearerAuth()) authMethod = 'bearer'; 426 | else if (hasApiKeyAuth()) authMethod = 'apikey'; 427 | 428 | // Prepare response object 429 | const responseObj: ResponseObject = { 430 | request: { 431 | url: fullUrl, 432 | method: config.method || 'GET', 433 | headers: { 434 | ...sanitizeHeaders(config.headers as Record, false), 435 | ...sanitizeHeaders(request.params.arguments.headers || {}, true) 436 | }, 437 | body: config.data, 438 | authMethod 439 | }, 440 | response: { 441 | statusCode: response.status, 442 | statusText: response.statusText, 443 | timing: `${endTime - startTime}ms`, 444 | headers: sanitizeHeaders(response.headers as Record, false), 445 | body: response.data, 446 | }, 447 | validation: { 448 | isError: response.status >= 400, 449 | messages: response.status >= 400 ? 450 | [`Request failed with status ${response.status}`] : 451 | ['Request completed successfully'] 452 | } 453 | }; 454 | 455 | // Check response body size independently 456 | const bodyStr = typeof response.data === 'string' 457 | ? response.data 458 | : JSON.stringify(response.data); 459 | const bodySize = Buffer.from(bodyStr).length; 460 | 461 | if (bodySize > RESPONSE_SIZE_LIMIT) { 462 | // Simply truncate to the size limit 463 | responseObj.response.body = bodyStr.slice(0, RESPONSE_SIZE_LIMIT); 464 | responseObj.validation.messages.push( 465 | `Response truncated: ${RESPONSE_SIZE_LIMIT} of ${bodySize} bytes returned due to size limit (${RESPONSE_SIZE_LIMIT} bytes)` 466 | ); 467 | responseObj.validation.truncated = { 468 | originalSize: bodySize, 469 | returnedSize: RESPONSE_SIZE_LIMIT, 470 | truncationPoint: RESPONSE_SIZE_LIMIT, 471 | sizeLimit: RESPONSE_SIZE_LIMIT 472 | }; 473 | } 474 | 475 | return { 476 | content: [ 477 | { 478 | type: 'text', 479 | text: JSON.stringify(responseObj, null, 2), 480 | }, 481 | ], 482 | }; 483 | } catch (error) { 484 | if (axios.isAxiosError(error)) { 485 | return { 486 | content: [ 487 | { 488 | type: 'text', 489 | text: JSON.stringify({ 490 | error: { 491 | message: error.message, 492 | code: error.code, 493 | request: { 494 | url: `${process.env.REST_BASE_URL}${normalizedEndpoint}`, 495 | method: config.method, 496 | headers: { 497 | ...sanitizeHeaders(config.headers as Record, false), 498 | ...sanitizeHeaders(request.params.arguments.headers || {}, true) 499 | }, 500 | body: config.data 501 | } 502 | } 503 | }, null, 2), 504 | }, 505 | ], 506 | isError: true, 507 | }; 508 | } 509 | throw error; 510 | } 511 | }); 512 | } 513 | 514 | async run() { 515 | await this.setupServer(); 516 | const transport = new StdioServerTransport(); 517 | await this.server.connect(transport); 518 | console.error('REST API Tester MCP server running on stdio'); 519 | } 520 | } 521 | 522 | const server = new RestTester(); 523 | server.run().catch(console.error); 524 | --------------------------------------------------------------------------------