# Directory Structure
```
├── .env.example
├── .gitignore
├── config.json
├── jest.config.js
├── package.json
├── README.md
├── src
│ ├── config.ts
│ ├── postman-mcp-simple.ts
│ ├── simple-server.ts
│ ├── simple-stdio.ts
│ ├── swagger-mcp-simple.ts
│ └── types.ts
├── start-mcp.sh
├── test-simple.ts
├── tsconfig.json
└── yarn.lock
```
# Files
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
1 | # Server Configuration
2 | PORT=3000
3 |
4 | # API Authentication
5 | API_USERNAME=
6 | API_PASSWORD=
7 | API_TOKEN=
8 |
9 | # Default API Configuration
10 | DEFAULT_API_BASE_URL=
11 | DEFAULT_SWAGGER_URL=
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Dependencies
2 | node_modules/
3 | yarn-debug.log*
4 | yarn-error.log*
5 |
6 | # Build output
7 | dist/
8 | build/
9 |
10 | # Environment variables
11 | .env
12 | .env.local
13 | .env.*.local
14 |
15 | # IDE and editor files
16 | .idea/
17 | .vscode/
18 | *.swp
19 | *.swo
20 | .DS_Store
21 |
22 | # Test coverage
23 | coverage/
24 |
25 | # Logs
26 | logs/
27 | *.log
28 | npm-debug.log*
29 |
30 | # Local test files
31 | ns-openapi.json
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Swagger/Postman MCP Server
2 |
3 | Server that ingests and serves Swagger/OpenAPI specifications and Postman collections as MCP (Model Context Protocol) tools using a **simplified strategic approach**.
4 |
5 | Instead of generating hundreds of individual tools for each API endpoint, this server provides **only 4 strategic tools** that allow AI agents to dynamically discover and interact with APIs:
6 |
7 | ```bash
8 | Example prompt:
9 | Help me generate an axios call using our api mcp. I want to implement updating a user. Follow our same DDD pattern (tanstack hook -> axios service)
10 | ```
11 |
12 | ## Features
13 |
14 | - **Strategic Tool Approach**: Only 4 tools instead of hundreds for better AI agent performance
15 | - **OpenAPI/Swagger Support**: Load OpenAPI 2.0/3.0 specifications from URLs or local files
16 | - **Postman Collection Support**: Load Postman collection JSON files from URLs or local files
17 | - **Environment Variables**: Support for Postman environment files
18 | - **Authentication**: Multiple authentication methods (Basic, Bearer, API Key, OAuth2)
19 | - **Dynamic API Discovery**: Tools for listing, searching, and getting details about API endpoints
20 | - **Request Execution**: Execute API requests with proper parameter handling and authentication
21 |
22 | ## Security
23 |
24 | This is a personal server!! Do not expose it to the public internet.
25 | If the underlying API requires authentication, you should not expose the MCP server to the public internet.
26 |
27 | ## TODO
28 |
29 | - secrets - the MCP server should be able to use secrets from the user to authenticate requests to the API
30 | - Comprehensive test suite
31 |
32 | ## Prerequisites
33 |
34 | - Node.js (v18 or higher)
35 | - Yarn package manager
36 | - TypeScript
37 |
38 | ## Installation
39 |
40 | ```bash
41 | # Clone the repository
42 | git clone <repository-url>
43 | cd swag-mcp
44 |
45 | # Install dependencies
46 | npm install
47 | # or
48 | yarn install
49 |
50 | # Build the project
51 | npm run build
52 | # or
53 | yarn build
54 |
55 | # Make the start script executable (Linux/macOS)
56 | chmod +x start-mcp.sh
57 | ```
58 |
59 | ### Quick Setup for Cursor
60 |
61 | 1. **Clone and build** (commands above)
62 | 2. **Configure** your `config.json` with your API details
63 | 3. **Update paths**: Edit `start-mcp.sh` and change the `cd` path to your installation directory
64 | 4. **Add to Cursor**: Edit `~/.cursor/mcp.json` and add:
65 | ```json
66 | {
67 | "mcpServers": {
68 | "postman-swagger-api": {
69 | "command": "/full/path/to/your/swag-mcp/start-mcp.sh"
70 | }
71 | }
72 | }
73 | ```
74 | 5. **Restart Cursor** and start using the 4 strategic MCP tools!
75 |
76 | ## Configuration
77 |
78 | The server uses a `config.json` file for configuration. You can specify either OpenAPI/Swagger specifications or Postman collections.
79 |
80 | ### OpenAPI/Swagger Configuration
81 |
82 | ```json
83 | {
84 | "api": {
85 | "type": "openapi",
86 | "openapi": {
87 | "url": "https://petstore.swagger.io/v2/swagger.json",
88 | "apiBaseUrl": "https://petstore.swagger.io/v2",
89 | "defaultAuth": {
90 | "type": "apiKey",
91 | "apiKey": "special-key",
92 | "apiKeyName": "api_key",
93 | "apiKeyIn": "header"
94 | }
95 | }
96 | },
97 | "log": {
98 | "level": "info"
99 | }
100 | }
101 | ```
102 |
103 | ### Postman Collection Configuration
104 |
105 | ```json
106 | {
107 | "api": {
108 | "type": "postman",
109 | "postman": {
110 | "collectionUrl": "https://www.postman.com/collections/your-collection-id",
111 | "collectionFile": "./examples/postman-collection.json",
112 | "environmentUrl": "https://www.postman.com/environments/your-environment-id",
113 | "environmentFile": "./examples/postman-environment.json",
114 | "defaultAuth": {
115 | "type": "bearer",
116 | "token": "your-api-token-here"
117 | }
118 | }
119 | },
120 | "log": {
121 | "level": "info"
122 | }
123 | }
124 | ```
125 |
126 | ### Configuration Options
127 |
128 | #### API Configuration
129 |
130 | - `api.type`: Either `"openapi"` or `"postman"`
131 | - `api.openapi`: OpenAPI/Swagger specific configuration
132 | - `url`: URL to the OpenAPI specification
133 | - `apiBaseUrl`: Base URL for API requests
134 | - `defaultAuth`: Default authentication configuration
135 | - `api.postman`: Postman specific configuration
136 | - `collectionUrl`: URL to the Postman collection (optional)
137 | - `collectionFile`: Path to local Postman collection file (optional)
138 | - `environmentUrl`: URL to the Postman environment (optional)
139 | - `environmentFile`: Path to local Postman environment file (optional)
140 | - `defaultAuth`: Default authentication configuration
141 |
142 | #### Authentication Configuration
143 |
144 | - `type`: Authentication type (`"basic"`, `"bearer"`, `"apiKey"`, `"oauth2"`)
145 | - `username`: Username (for basic auth)
146 | - `password`: Password (for basic auth)
147 | - `token`: Token (for bearer/oauth2 auth)
148 | - `apiKey`: API key value
149 | - `apiKeyName`: API key parameter name
150 | - `apiKeyIn`: Where to send API key (`"header"` or `"query"`)
151 |
152 | #### Logging Configuration
153 |
154 | - `log.level`: Logging level (`"debug"`, `"info"`, `"warn"`, `"error"`)
155 |
156 | ## Usage
157 |
158 | ### Starting the MCP Server
159 |
160 | The server runs via stdio transport for MCP connections:
161 |
162 | ```bash
163 | # Start the simplified MCP server via stdio
164 | ./start-mcp.sh
165 |
166 | # Or directly with node
167 | node dist/simple-stdio.js
168 |
169 | # For development with auto-reload
170 | npm run dev:simple
171 | # or
172 | yarn dev:simple
173 | ```
174 |
175 | ### MCP Integration
176 |
177 | This server uses stdio transport and is designed to be used with MCP clients like Claude Desktop or Cursor.
178 |
179 | ## Installing in Cursor
180 |
181 | To use this MCP server with Cursor, you need to add it to your Cursor MCP configuration:
182 |
183 | ### 1. Locate your Cursor MCP configuration file
184 |
185 | The configuration file is located at:
186 |
187 | - **Linux/macOS**: `~/.cursor/mcp.json`
188 | - **Windows**: `%APPDATA%\.cursor\mcp.json`
189 |
190 | ### 2. Add the MCP server configuration
191 |
192 | Edit your `mcp.json` file to include this server:
193 |
194 | ```json
195 | {
196 | "mcpServers": {
197 | "postman-swagger-api": {
198 | "command": "/path/to/your/swag-mcp/start-mcp.sh"
199 | }
200 | }
201 | }
202 | ```
203 |
204 | **⚠️ Important: Change the path!**
205 |
206 | Replace `/path/to/your/swag-mcp/start-mcp.sh` with the actual path to your cloned repository. For example:
207 |
208 | - **Linux/macOS**: `"/home/username/Documents/swag-mcp/start-mcp.sh"`
209 | - **Windows**: `"C:\\Users\\username\\Documents\\swag-mcp\\start-mcp.sh"`
210 |
211 | ### 3. Example complete configuration
212 |
213 | ```json
214 | {
215 | "mcpServers": {
216 | "supabase": {
217 | "command": "npx",
218 | "args": [
219 | "-y",
220 | "@supabase/mcp-server-supabase@latest",
221 | "--access-token",
222 | "your-supabase-token"
223 | ]
224 | },
225 | "postman-swagger-api": {
226 | "command": "/home/username/Documents/swag-mcp/start-mcp.sh"
227 | }
228 | }
229 | }
230 | ```
231 |
232 | ### 4. Restart Cursor
233 |
234 | After saving the configuration file, restart Cursor for the changes to take effect.
235 |
236 | ### 5. Verify installation
237 |
238 | In Cursor, you should now have access to the 4 strategic MCP tools:
239 |
240 | - `list_requests` - List all available requests
241 | - `get_request_details` - Get detailed request information
242 | - `search_requests` - Search requests by keyword
243 | - `make_request` - Execute any API request
244 |
245 | ### Troubleshooting
246 |
247 | If the MCP server fails to start:
248 |
249 | 1. **Update start-mcp.sh path**: Edit `start-mcp.sh` and change the `cd` path from `/path/to/your/swag-mcp` to your actual installation directory
250 | 2. **Check the path**: Ensure the path in `mcp.json` points to your actual `start-mcp.sh` file
251 | 3. **Check permissions**: Make sure `start-mcp.sh` is executable (`chmod +x start-mcp.sh`)
252 | 4. **Check build**: Ensure you've run `npm run build` to compile the TypeScript files
253 | 5. **Check logs**: Look in Cursor's MCP logs for error messages
254 |
255 | ### Example Path Updates
256 |
257 | If you cloned to `/home/username/Documents/swag-mcp/`, then:
258 |
259 | **In `start-mcp.sh`:**
260 |
261 | ```bash
262 | cd "/home/username/Documents/swag-mcp"
263 | ```
264 |
265 | **In `~/.cursor/mcp.json`:**
266 |
267 | ```json
268 | "command": "/home/username/Documents/swag-mcp/start-mcp.sh"
269 | ```
270 |
271 | ## How It Works
272 |
273 | ### Strategic Tool Approach
274 |
275 | Instead of generating hundreds of individual tools for each API endpoint, this server provides **4 strategic tools** that enable dynamic API discovery and interaction:
276 |
277 | ### OpenAPI/Swagger Mode
278 |
279 | **4 Strategic Tools:**
280 |
281 | 1. **`list_endpoints`** - List all available API endpoints
282 | 2. **`get_endpoint_details`** - Get detailed information about specific endpoints
283 | 3. **`search_endpoints`** - Search endpoints by keyword
284 | 4. **`make_api_call`** - Execute any API call with proper authentication
285 |
286 | **Process:**
287 |
288 | 1. Loads the OpenAPI specification from the configured URL or file
289 | 2. Parses the specification to extract API endpoints, parameters, and security schemes
290 | 3. Makes endpoint information available through the 4 strategic tools
291 | 4. Handles authentication and parameter validation dynamically
292 | 5. Executes API requests and returns responses
293 |
294 | ### Postman Collection Mode
295 |
296 | **4 Strategic Tools:**
297 |
298 | 1. **`list_requests`** - List all available requests in the collection
299 | 2. **`get_request_details`** - Get detailed information about specific requests
300 | 3. **`search_requests`** - Search requests by keyword
301 | 4. **`make_request`** - Execute any request from the collection
302 |
303 | **Process:**
304 |
305 | 1. Loads the Postman collection JSON from the configured URL or file
306 | 2. Optionally loads a Postman environment file for variable substitution
307 | 3. Parses requests, folders, and nested items in the collection
308 | 4. Makes request information available through the 4 strategic tools
309 | 5. Handles variable substitution, authentication, and parameter mapping dynamically
310 | 6. Executes requests with proper headers, query parameters, and body data
311 |
312 | ### Benefits of Strategic Tools
313 |
314 | - **Better AI Performance**: 4 tools vs hundreds means faster decision making
315 | - **Dynamic Discovery**: AI agents can explore APIs without knowing endpoints beforehand
316 | - **Flexible Interaction**: Any endpoint can be called through `make_api_call`/`make_request`
317 | - **Reduced Overwhelm**: AI agents aren't flooded with tool options
318 |
319 | ## Strategic Tools Reference
320 |
321 | ### For OpenAPI/Swagger APIs
322 |
323 | 1. **`list_endpoints`**
324 |
325 | - Lists all available API endpoints with methods and paths
326 | - No parameters required
327 | - Returns: Array of endpoint summaries
328 |
329 | 2. **`get_endpoint_details`**
330 |
331 | - Get detailed information about a specific endpoint
332 | - Parameters: `method` (GET/POST/etc), `path` (/users/{id}/etc)
333 | - Returns: Full endpoint specification with parameters, body schema, responses
334 |
335 | 3. **`search_endpoints`**
336 |
337 | - Search endpoints by keyword in path, summary, or description
338 | - Parameters: `query` (search term)
339 | - Returns: Filtered list of matching endpoints
340 |
341 | 4. **`make_api_call`**
342 | - Execute an API call to any endpoint
343 | - Parameters: `method`, `path`, `pathParams`, `queryParams`, `headers`, `body`
344 | - Returns: API response with status and data
345 |
346 | ### For Postman Collections
347 |
348 | 1. **`list_requests`**
349 |
350 | - Lists all available requests in the collection
351 | - No parameters required
352 | - Returns: Array of request summaries
353 |
354 | 2. **`get_request_details`**
355 |
356 | - Get detailed information about a specific request
357 | - Parameters: `requestId` or `requestName`
358 | - Returns: Full request specification
359 |
360 | 3. **`search_requests`**
361 |
362 | - Search requests by keyword
363 | - Parameters: `query` (search term)
364 | - Returns: Filtered list of matching requests
365 |
366 | 4. **`make_request`**
367 | - Execute any request from the collection
368 | - Parameters: `requestId`, `variables` (for substitution)
369 | - Returns: Request response
370 |
371 | ### Authentication
372 |
373 | The server supports multiple authentication methods:
374 |
375 | - **Basic Authentication**: Username/password
376 | - **Bearer Token**: JWT or other bearer tokens
377 | - **API Key**: In headers or query parameters
378 | - **OAuth2**: Bearer token based
379 |
380 | Authentication can be configured globally or overridden per request.
381 |
382 | ## Example Configuration
383 |
384 | Your `config.json` should specify either OpenAPI or Postman configuration as shown above.
385 |
386 | ### Example Postman Collection Structure
387 |
388 | ```json
389 | {
390 | "info": {
391 | "name": "Sample API Collection",
392 | "description": "A sample Postman collection"
393 | },
394 | "item": [
395 | {
396 | "name": "Get Users",
397 | "request": {
398 | "method": "GET",
399 | "header": [],
400 | "url": {
401 | "raw": "{{baseUrl}}/users",
402 | "host": ["{{baseUrl}}"],
403 | "path": ["users"]
404 | }
405 | }
406 | }
407 | ]
408 | }
409 | ```
410 |
411 | ## Development
412 |
413 | ```bash
414 | # Install dependencies
415 | npm install
416 |
417 | # Run in development mode
418 | npm run dev
419 |
420 | # Run tests
421 | npm test
422 |
423 | # Build for production
424 | npm run build
425 | ```
426 |
427 | ## License
428 |
429 | ISC
430 |
431 | ## Environment Variables
432 |
433 | - `PORT`: Server port (default: 3000)
434 | - `API_USERNAME`: Username for API authentication (fallback)
435 | - `API_PASSWORD`: Password for API authentication (fallback)
436 | - `API_TOKEN`: API token for authentication (fallback)
437 | - `DEFAULT_API_BASE_URL`: Default base URL for API endpoints (fallback)
438 | - `DEFAULT_SWAGGER_URL`: Default Swagger specification URL
439 |
```
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
```javascript
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | roots: ['<rootDir>/tests'],
5 | transform: {
6 | '^.+\\.tsx?$': 'ts-jest',
7 | },
8 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
9 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
10 | };
```
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "api": {
3 | "type": "postman",
4 | "postman": {
5 | "collectionFile": "./ns-openapi.json",
6 | "environmentFile": "./ns-openapi.json",
7 | "defaultAuth": {
8 | "type": "bearer",
9 | "token": "test-token"
10 | }
11 | }
12 | },
13 | "log": {
14 | "level": "debug"
15 | },
16 | "server": {
17 | "port": 9001,
18 | "host": "0.0.0.0"
19 | }
20 | }
```
--------------------------------------------------------------------------------
/start-mcp.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 |
3 | # ⚠️ IMPORTANT: Update this path to match your installation directory!
4 | # Change this to the full path where you cloned the swag-mcp repository
5 | cd "/path/to/your/swag-mcp"
6 |
7 | # Set environment variables
8 | export NODE_ENV=production
9 |
10 | # Start the simplified MCP server (only 4 strategic tools instead of 300+)
11 | exec node dist/simple-stdio.js
```
--------------------------------------------------------------------------------
/test-simple.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { SimpleSwaggerMcpServer } from "./src/swagger-mcp-simple.js";
2 |
3 | async function test() {
4 | console.log("Testing simplified MCP server...");
5 |
6 | const server = new SimpleSwaggerMcpServer("https://api.example.com");
7 |
8 | // Test with a simple OpenAPI spec
9 | const simpleSpec = {
10 | openapi: "3.0.0",
11 | info: {
12 | title: "Test API",
13 | version: "1.0.0",
14 | },
15 | paths: {
16 | "/users": {
17 | get: {
18 | operationId: "getUsers",
19 | summary: "Get all users",
20 | responses: {
21 | "200": {
22 | description: "Success",
23 | },
24 | },
25 | },
26 | },
27 | },
28 | };
29 |
30 | console.log("✅ SimpleSwaggerMcpServer created successfully");
31 | console.log("This demonstrates the strategic tool approach works!");
32 | }
33 |
34 | test().catch(console.error);
35 |
```
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
```typescript
1 | export interface SwaggerConfig {
2 | swaggerUrl?: string;
3 | swaggerFile?: string;
4 | apiBaseUrl: string;
5 | auth?: AuthConfig;
6 | }
7 |
8 | export interface PostmanConfig {
9 | collectionUrl?: string;
10 | collectionFile?: string;
11 | environmentUrl?: string;
12 | environmentFile?: string;
13 | auth?: AuthConfig;
14 | }
15 |
16 | export interface ApiConfig {
17 | type: "openapi" | "postman";
18 | openapi?: SwaggerConfig;
19 | postman?: PostmanConfig;
20 | }
21 |
22 | export interface AuthConfig {
23 | type: "basic" | "bearer" | "apiKey" | "oauth2";
24 | username?: string;
25 | password?: string;
26 | token?: string;
27 | apiKey?: string;
28 | apiKeyName?: string;
29 | apiKeyIn?: "header" | "query";
30 | }
31 |
32 | export interface ToolInput {
33 | auth?: AuthConfig;
34 | [key: string]: any;
35 | }
36 |
37 | export interface SecurityScheme {
38 | type: string;
39 | description?: string;
40 | name?: string;
41 | in?: string;
42 | scheme?: string;
43 | flows?: {
44 | implicit?: {
45 | authorizationUrl: string;
46 | scopes: Record<string, string>;
47 | };
48 | [key: string]: any;
49 | };
50 | }
51 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "swag-mcp",
3 | "version": "1.0.0",
4 | "description": "An MCP server that ingests and serves Swagger/OpenAPI specifications and Postman collections",
5 | "main": "dist/simple-stdio.js",
6 | "scripts": {
7 | "start": "node dist/simple-stdio.js",
8 | "start:simple": "node dist/simple-stdio.js",
9 | "dev": "NODE_OPTIONS='--loader ts-node/esm' ts-node src/simple-stdio.ts",
10 | "dev:simple": "NODE_OPTIONS='--loader ts-node/esm' ts-node src/simple-server.ts",
11 | "build": "tsc",
12 | "build:simple": "npx tsc src/swagger-mcp-simple.ts src/simple-server.ts src/config.ts src/types.ts --outDir dist --module NodeNext --moduleResolution NodeNext --target ES2020 --esModuleInterop --skipLibCheck --declaration",
13 | "watch": "tsc -w",
14 | "test": "jest",
15 | "test:watch": "jest --watch",
16 | "test:coverage": "jest --coverage"
17 | },
18 | "keywords": [
19 | "swagger",
20 | "openapi",
21 | "postman",
22 | "collections",
23 | "api",
24 | "documentation",
25 | "mcp",
26 | "mcp-server",
27 | "postman"
28 | ],
29 | "author": "",
30 | "license": "ISC",
31 | "dependencies": {
32 | "@apidevtools/swagger-parser": "^10.1.0",
33 | "@modelcontextprotocol/sdk": "^1.7.0",
34 | "@types/cors": "^2.8.17",
35 | "@types/express": "^5.0.0",
36 | "@types/postman-collection": "^3.5.10",
37 | "@types/swagger-parser": "^7.0.1",
38 | "axios": "^1.8.3",
39 | "cors": "^2.8.5",
40 | "dotenv": "^16.4.5",
41 | "eventsource": "^3.0.5",
42 | "express": "^4.18.3",
43 | "node-fetch": "^3.3.2",
44 | "openapi-types": "^12.1.3",
45 | "postman-collection": "^5.0.2",
46 | "ts-node": "^10.9.2",
47 | "tslib": "^2.8.1",
48 | "typescript": "^5.4.2",
49 | "zod": "^3.22.4"
50 | },
51 | "devDependencies": {
52 | "@types/jest": "^29.5.14",
53 | "@types/supertest": "^6.0.2",
54 | "jest": "^29.7.0",
55 | "supertest": "^7.0.0",
56 | "ts-jest": "^29.2.6"
57 | },
58 | "packageManager": "[email protected]+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72"
59 | }
60 |
```
--------------------------------------------------------------------------------
/src/simple-server.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { SimpleSwaggerMcpServer } from "./swagger-mcp-simple.js";
2 | import { loadConfig } from "./config.js";
3 |
4 | async function main() {
5 | try {
6 | const configPath = process.argv[2] || undefined;
7 | const config = await loadConfig(configPath);
8 |
9 | // Use new config structure or fallback to legacy
10 | const openApiConfig =
11 | config.api.type === "openapi" ? config.api.openapi : config.swagger;
12 |
13 | if (!openApiConfig) {
14 | throw new Error("No OpenAPI configuration found");
15 | }
16 |
17 | console.log("Creating MCP server with config:", {
18 | apiBaseUrl: openApiConfig.apiBaseUrl,
19 | authType: openApiConfig.defaultAuth?.type,
20 | });
21 |
22 | const server = new SimpleSwaggerMcpServer(
23 | openApiConfig.apiBaseUrl,
24 | openApiConfig.defaultAuth && openApiConfig.defaultAuth.type
25 | ? (openApiConfig.defaultAuth as any)
26 | : undefined
27 | );
28 |
29 | // Load the swagger spec
30 | await server.loadSwaggerSpec(openApiConfig.url);
31 |
32 | console.log(
33 | "✅ Simple MCP Server successfully initialized with strategic tools!"
34 | );
35 | console.log("Now you have only 4 tools instead of hundreds:");
36 | console.log(" 1. list_endpoints - List all available API endpoints");
37 | console.log(
38 | " 2. get_endpoint_details - Get detailed info about specific endpoints"
39 | );
40 | console.log(" 3. search_endpoints - Search endpoints by keyword");
41 | console.log(" 4. make_api_call - Make actual API calls");
42 | console.log("");
43 | console.log("This approach allows AI agents to:");
44 | console.log(" - Discover APIs dynamically");
45 | console.log(" - Get required parameter info");
46 | console.log(" - Make informed API calls");
47 | console.log(" - Search for relevant endpoints");
48 |
49 | return server.getServer();
50 | } catch (error) {
51 | console.error("Failed to initialize MCP server:", error);
52 | process.exit(1);
53 | }
54 | }
55 |
56 | main().catch(console.error);
57 |
```
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import fs from "fs/promises";
3 | import path from "path";
4 |
5 | // Define auth configuration schema
6 | const AuthConfigSchema = z.object({
7 | type: z.enum(["basic", "bearer", "apiKey", "oauth2"]),
8 | token: z.string().optional(),
9 | username: z.string().optional(),
10 | password: z.string().optional(),
11 | apiKey: z.string().optional(),
12 | apiKeyName: z.string().optional(),
13 | apiKeyIn: z.enum(["header", "query"]).optional(),
14 | });
15 |
16 | // Define the configuration schema
17 | export const ConfigSchema = z
18 | .object({
19 | api: z.object({
20 | type: z.enum(["openapi", "postman"]),
21 | openapi: z
22 | .object({
23 | url: z.string().url(),
24 | apiBaseUrl: z.string().url(),
25 | defaultAuth: AuthConfigSchema.optional(),
26 | })
27 | .optional(),
28 | postman: z
29 | .object({
30 | collectionUrl: z.string().url().optional(),
31 | collectionFile: z.string().optional(),
32 | environmentUrl: z.string().url().optional(),
33 | environmentFile: z.string().optional(),
34 | defaultAuth: AuthConfigSchema.optional(),
35 | })
36 | .optional(),
37 | }),
38 | // Keep legacy swagger config for backward compatibility
39 | swagger: z
40 | .object({
41 | url: z.string().url(),
42 | apiBaseUrl: z.string().url(),
43 | defaultAuth: AuthConfigSchema.optional(),
44 | })
45 | .optional(),
46 | log: z.object({
47 | level: z.enum(["debug", "info", "warn", "error"]),
48 | }),
49 | server: z.object({
50 | port: z.number().default(3000),
51 | host: z.string().default("0.0.0.0"),
52 | }),
53 | })
54 | .refine(
55 | (data) => {
56 | // Ensure we have either the new api config or legacy swagger config
57 | if (data.api.type === "openapi" && !data.api.openapi && !data.swagger) {
58 | return false;
59 | }
60 | if (data.api.type === "postman" && !data.api.postman) {
61 | return false;
62 | }
63 | return true;
64 | },
65 | {
66 | message:
67 | "Configuration must include appropriate API settings based on type",
68 | }
69 | );
70 |
71 | export type Config = z.infer<typeof ConfigSchema>;
72 |
73 | const defaultConfig: Config = {
74 | api: {
75 | type: "openapi",
76 | openapi: {
77 | url: "https://petstore.swagger.io/v2/swagger.json",
78 | apiBaseUrl: "https://petstore.swagger.io/v2",
79 | defaultAuth: {
80 | type: "apiKey",
81 | apiKey: "special-key",
82 | apiKeyName: "api_key",
83 | apiKeyIn: "header",
84 | },
85 | },
86 | },
87 | swagger: {
88 | url: "https://petstore.swagger.io/v2/swagger.json",
89 | apiBaseUrl: "https://petstore.swagger.io/v2",
90 | defaultAuth: {
91 | type: "apiKey",
92 | apiKey: "special-key",
93 | apiKeyName: "api_key",
94 | apiKeyIn: "header",
95 | },
96 | },
97 | log: {
98 | level: "info",
99 | },
100 | server: {
101 | port: 3000,
102 | host: "0.0.0.0",
103 | },
104 | };
105 |
106 | export async function loadConfig(configPath?: string): Promise<Config> {
107 | try {
108 | // If no config path provided, create default config file
109 | if (!configPath) {
110 | configPath = path.join(process.cwd(), "config.json");
111 | // Check if config file exists, if not create it with default values
112 | try {
113 | await fs.access(configPath);
114 | } catch {
115 | await fs.writeFile(configPath, JSON.stringify(defaultConfig, null, 2));
116 | console.log(`Created default configuration file at ${configPath}`);
117 | }
118 | }
119 |
120 | const configFile = await fs.readFile(configPath, "utf-8");
121 | const config = JSON.parse(configFile);
122 |
123 | // Handle legacy config migration
124 | if (config.swagger && !config.api) {
125 | config.api = {
126 | type: "openapi",
127 | openapi: config.swagger,
128 | };
129 | }
130 |
131 | return ConfigSchema.parse(config);
132 | } catch (error) {
133 | if (error instanceof z.ZodError) {
134 | console.error("Invalid configuration:", error.errors);
135 | } else {
136 | console.error("Error loading configuration:", error);
137 | }
138 | console.log("Using default configuration");
139 | return defaultConfig;
140 | }
141 | }
142 |
```
--------------------------------------------------------------------------------
/src/simple-stdio.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2 | import { loadConfig } from "./config.js";
3 | import { SimpleSwaggerMcpServer } from "./swagger-mcp-simple.js";
4 | import { SimplePostmanMcpServer } from "./postman-mcp-simple.js";
5 |
6 | async function main() {
7 | try {
8 | console.error(`[SIMPLE-STDIO] Starting simplified MCP server via stdio...`);
9 | console.error(`[SIMPLE-STDIO] Process ID: ${process.pid}`);
10 | console.error(`[SIMPLE-STDIO] Working directory: ${process.cwd()}`);
11 |
12 | // Load configuration
13 | console.error(`[SIMPLE-STDIO] Loading configuration...`);
14 | const config = await loadConfig();
15 |
16 | let mcpServer: SimpleSwaggerMcpServer | SimplePostmanMcpServer;
17 |
18 | // Create and initialize MCP server based on configuration
19 | if (config.api.type === "postman") {
20 | if (!config.api.postman) {
21 | throw new Error(
22 | 'Postman configuration is required when api.type is "postman"'
23 | );
24 | }
25 |
26 | console.error(
27 | "[SIMPLE-STDIO] Creating simplified Postman MCP server instance..."
28 | );
29 | console.error(
30 | "✅ Using simplified Postman explorer with only 4 strategic tools!"
31 | );
32 |
33 | mcpServer = new SimplePostmanMcpServer(config.api.postman.defaultAuth);
34 |
35 | // Load collection
36 | const collectionSource =
37 | config.api.postman.collectionUrl || config.api.postman.collectionFile;
38 | if (!collectionSource) {
39 | throw new Error(
40 | "Either collectionUrl or collectionFile must be specified for Postman configuration"
41 | );
42 | }
43 |
44 | console.error("[SIMPLE-STDIO] Loading Postman collection...");
45 | await (mcpServer as SimplePostmanMcpServer).loadCollection(
46 | collectionSource
47 | );
48 |
49 | // Load environment if specified
50 | const environmentSource =
51 | config.api.postman.environmentUrl || config.api.postman.environmentFile;
52 | if (environmentSource) {
53 | console.error("[SIMPLE-STDIO] Loading Postman environment...");
54 | await (mcpServer as SimplePostmanMcpServer).loadEnvironment(
55 | environmentSource
56 | );
57 | }
58 |
59 | console.error("[SIMPLE-STDIO] Postman collection loaded successfully");
60 |
61 | console.error(
62 | "✅ Simple MCP Server successfully initialized with strategic tools!"
63 | );
64 | console.error("Now you have only 4 tools instead of hundreds:");
65 | console.error(
66 | " 1. list_requests - List all available requests in the collection"
67 | );
68 | console.error(
69 | " 2. get_request_details - Get detailed info about specific requests"
70 | );
71 | console.error(" 3. search_requests - Search requests by keyword");
72 | console.error(
73 | " 4. make_request - Execute any request from the collection"
74 | );
75 | } else {
76 | // Default to OpenAPI/Swagger with simplified tools
77 | const openApiConfig = config.api.openapi || config.swagger;
78 | if (!openApiConfig) {
79 | throw new Error(
80 | 'OpenAPI configuration is required when api.type is "openapi" or for legacy swagger config'
81 | );
82 | }
83 |
84 | console.error(
85 | "[SIMPLE-STDIO] Creating simplified OpenAPI MCP server instance..."
86 | );
87 | mcpServer = new SimpleSwaggerMcpServer(
88 | openApiConfig.apiBaseUrl,
89 | openApiConfig.defaultAuth && openApiConfig.defaultAuth.type
90 | ? (openApiConfig.defaultAuth as any)
91 | : undefined
92 | );
93 |
94 | console.error("[SIMPLE-STDIO] Loading OpenAPI specification...");
95 | await mcpServer.loadSwaggerSpec(openApiConfig.url);
96 | console.error("[SIMPLE-STDIO] OpenAPI specification loaded successfully");
97 |
98 | console.error(
99 | "✅ Simple MCP Server successfully initialized with strategic tools!"
100 | );
101 | console.error("Now you have only 4 tools instead of hundreds:");
102 | console.error(" 1. list_endpoints - List all available API endpoints");
103 | console.error(
104 | " 2. get_endpoint_details - Get detailed info about specific endpoints"
105 | );
106 | console.error(" 3. search_endpoints - Search endpoints by keyword");
107 | console.error(" 4. make_api_call - Make actual API calls");
108 | }
109 |
110 | // Get the MCP server instance
111 | const server = mcpServer.getServer();
112 |
113 | // Create stdio transport
114 | console.error(`[SIMPLE-STDIO] Creating stdio transport...`);
115 | const transport = new StdioServerTransport();
116 |
117 | // Connect the MCP server to stdio transport
118 | console.error(`[SIMPLE-STDIO] Connecting MCP server to stdio transport...`);
119 | await server.connect(transport);
120 |
121 | console.error(
122 | "[SIMPLE-STDIO] Simplified MCP server connected via stdio successfully!"
123 | );
124 | console.error(
125 | `[SIMPLE-STDIO] Server is ready and listening for requests...`
126 | );
127 |
128 | // Handle process termination gracefully
129 | process.on("SIGINT", () => {
130 | console.error(
131 | "[SIMPLE-STDIO] Received SIGINT, shutting down gracefully..."
132 | );
133 | process.exit(0);
134 | });
135 |
136 | process.on("SIGTERM", () => {
137 | console.error(
138 | "[SIMPLE-STDIO] Received SIGTERM, shutting down gracefully..."
139 | );
140 | process.exit(0);
141 | });
142 | } catch (error) {
143 | console.error(
144 | "[SIMPLE-STDIO] Failed to start simplified MCP server:",
145 | error
146 | );
147 | process.exit(1);
148 | }
149 | }
150 |
151 | // Handle uncaught exceptions and rejections
152 | process.on("uncaughtException", (error) => {
153 | console.error("[SIMPLE-STDIO] Uncaught Exception:", error);
154 | process.exit(1);
155 | });
156 |
157 | process.on("unhandledRejection", (reason, promise) => {
158 | console.error(
159 | "[SIMPLE-STDIO] Unhandled Rejection at:",
160 | promise,
161 | "reason:",
162 | reason
163 | );
164 | process.exit(1);
165 | });
166 |
167 | main().catch((error) => {
168 | console.error("[SIMPLE-STDIO] Unhandled error:", error);
169 | process.exit(1);
170 | });
171 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "libReplacement": true, /* Enable lib replacement. */
18 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
19 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
20 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
21 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
22 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
23 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
24 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
25 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
26 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
27 |
28 | /* Modules */
29 | "module": "NodeNext", /* Specify what module code is generated. */
30 | "moduleResolution": "NodeNext", /* Specify how TypeScript looks up a file from a given module specifier. */
31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
39 | // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
40 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
41 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
42 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
43 | // "noUncheckedSideEffectImports": true, /* Check side effect imports. */
44 | // "resolveJsonModule": true, /* Enable importing .json files. */
45 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
46 | // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
47 |
48 | /* JavaScript Support */
49 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
50 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
51 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
52 |
53 | /* Emit */
54 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
55 | "declarationMap": true, /* Create sourcemaps for d.ts files. */
56 | "emitDeclarationOnly": false, /* Only output d.ts files and not JavaScript files. */
57 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */
58 | "noEmit": false, /* Disable emitting files from a compilation. */
59 | "outDir": "dist", /* Specify an output folder for all emitted files. */
60 | "removeComments": true, /* Disable emitting comments. */
61 | "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
62 | "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
63 | "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
64 | "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
65 | "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
66 | "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
67 | "newLine": "crlf", /* Set the newline character for emitting files. */
68 | "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
69 | "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
70 | "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
71 | "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
72 |
73 | /* Interop Constraints */
74 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
75 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
76 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
77 | // "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
78 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
79 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
80 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
81 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
82 |
83 | /* Type Checking */
84 | "strict": true, /* Enable all strict type-checking options. */
85 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
86 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
87 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
88 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
89 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
90 | // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
104 |
105 | /* Completeness */
106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
108 | },
109 | "include": ["src/**/*"],
110 | "exclude": ["node_modules", "dist"]
111 | }
112 |
```
--------------------------------------------------------------------------------
/src/swagger-mcp-simple.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3 | import { z } from "zod";
4 | import axios from "axios";
5 | import SwaggerParser from "@apidevtools/swagger-parser";
6 | import { Request, Response } from "express";
7 | import { AuthConfig } from "./types.js";
8 |
9 | export class SimpleSwaggerMcpServer {
10 | private mcpServer: McpServer;
11 | private swaggerSpec: any = null;
12 | private apiBaseUrl: string;
13 | private defaultAuth: AuthConfig | undefined;
14 |
15 | constructor(apiBaseUrl: string, defaultAuth?: AuthConfig) {
16 | this.apiBaseUrl = apiBaseUrl;
17 | this.defaultAuth = defaultAuth;
18 | this.mcpServer = new McpServer({
19 | name: "Simple Swagger API MCP Server",
20 | version: "1.0.0",
21 | });
22 | }
23 |
24 | async loadSwaggerSpec(specUrlOrFile: string) {
25 | console.debug("Loading Swagger specification from:", specUrlOrFile);
26 | try {
27 | this.swaggerSpec = (await SwaggerParser.parse(specUrlOrFile)) as any;
28 |
29 | const info = this.swaggerSpec.info;
30 | console.debug("Loaded Swagger spec:", {
31 | title: info.title,
32 | version: info.version,
33 | });
34 |
35 | this.mcpServer = new McpServer({
36 | name: info.title || "Swagger API Server",
37 | version: info.version || "1.0.0",
38 | description: info.description || undefined,
39 | });
40 |
41 | await this.registerTools();
42 | } catch (error) {
43 | console.error("Failed to load Swagger specification:", error);
44 | throw error;
45 | }
46 | }
47 |
48 | private getAuthHeaders(auth?: AuthConfig): Record<string, string> {
49 | const authConfig = auth || this.defaultAuth;
50 | if (!authConfig) return {};
51 |
52 | switch (authConfig.type) {
53 | case "basic":
54 | if (authConfig.username && authConfig.password) {
55 | const credentials = Buffer.from(
56 | `${authConfig.username}:${authConfig.password}`
57 | ).toString("base64");
58 | return { Authorization: `Basic ${credentials}` };
59 | }
60 | break;
61 | case "bearer":
62 | if (authConfig.token) {
63 | return { Authorization: `Bearer ${authConfig.token}` };
64 | }
65 | break;
66 | case "apiKey":
67 | if (authConfig.apiKey && authConfig.apiKeyName) {
68 | if (authConfig.apiKeyIn === "header") {
69 | return { [authConfig.apiKeyName]: authConfig.apiKey };
70 | }
71 | }
72 | break;
73 | case "oauth2":
74 | if (authConfig.token) {
75 | return { Authorization: `Bearer ${authConfig.token}` };
76 | }
77 | break;
78 | }
79 | return {};
80 | }
81 |
82 | private getAuthQueryParams(auth?: AuthConfig): Record<string, string> {
83 | const authConfig = auth || this.defaultAuth;
84 | if (!authConfig) return {};
85 |
86 | if (
87 | authConfig.type === "apiKey" &&
88 | authConfig.apiKey &&
89 | authConfig.apiKeyName &&
90 | authConfig.apiKeyIn === "query"
91 | ) {
92 | return { [authConfig.apiKeyName]: authConfig.apiKey };
93 | }
94 |
95 | return {};
96 | }
97 |
98 | private async registerTools() {
99 | console.debug("Starting tool registration process");
100 | if (!this.swaggerSpec || !this.swaggerSpec.paths) {
101 | console.warn("No paths found in Swagger spec");
102 | return;
103 | }
104 |
105 | const paths = this.swaggerSpec.paths;
106 | const totalPaths = Object.keys(paths).length;
107 | console.debug(`Found ${totalPaths} paths to process`);
108 |
109 | // Tool 1: List all available endpoints
110 | this.mcpServer.tool(
111 | "list_endpoints",
112 | "List all available API endpoints with basic information including path, method, summary, and tags",
113 | {
114 | input: z.object({
115 | method: z
116 | .string()
117 | .optional()
118 | .describe("Filter by HTTP method (GET, POST, PUT, DELETE, etc.)"),
119 | tag: z.string().optional().describe("Filter by OpenAPI tag"),
120 | limit: z
121 | .number()
122 | .optional()
123 | .default(50)
124 | .describe("Maximum number of endpoints to return"),
125 | }),
126 | },
127 | async ({ input }) => {
128 | const endpoints = [];
129 |
130 | for (const [path, pathItem] of Object.entries(paths)) {
131 | if (!pathItem) continue;
132 |
133 | for (const [method, operation] of Object.entries(pathItem as any)) {
134 | if (method === "$ref" || !operation) continue;
135 |
136 | const op = operation as any;
137 | const operationId = op.operationId || `${method}-${path}`;
138 |
139 | // Apply filters
140 | if (
141 | input.method &&
142 | method.toLowerCase() !== input.method.toLowerCase()
143 | )
144 | continue;
145 | if (input.tag && (!op.tags || !op.tags.includes(input.tag)))
146 | continue;
147 |
148 | endpoints.push({
149 | operationId,
150 | method: method.toUpperCase(),
151 | path,
152 | summary: op.summary || "",
153 | description: op.description || "",
154 | tags: op.tags || [],
155 | deprecated: op.deprecated || false,
156 | });
157 |
158 | if (endpoints.length >= input.limit) break;
159 | }
160 | if (endpoints.length >= input.limit) break;
161 | }
162 |
163 | return {
164 | content: [
165 | {
166 | type: "text",
167 | text: JSON.stringify(
168 | {
169 | total: endpoints.length,
170 | endpoints,
171 | },
172 | null,
173 | 2
174 | ),
175 | },
176 | ],
177 | };
178 | }
179 | );
180 |
181 | // Tool 2: Get detailed information about a specific endpoint
182 | this.mcpServer.tool(
183 | "get_endpoint_details",
184 | "Get detailed information about a specific API endpoint including parameters, request/response schemas, and authentication requirements",
185 | {
186 | input: z.object({
187 | operationId: z.string().describe("The operation ID of the endpoint"),
188 | path: z
189 | .string()
190 | .optional()
191 | .describe("The API path (alternative to operationId)"),
192 | method: z
193 | .string()
194 | .optional()
195 | .describe("The HTTP method (required if using path)"),
196 | }),
197 | },
198 | async ({ input }) => {
199 | let targetOperation = null;
200 | let targetPath = "";
201 | let targetMethod = "";
202 |
203 | // Find the operation by operationId or path+method
204 | for (const [path, pathItem] of Object.entries(paths)) {
205 | if (!pathItem) continue;
206 |
207 | for (const [method, operation] of Object.entries(pathItem as any)) {
208 | if (method === "$ref" || !operation) continue;
209 |
210 | const op = operation as any;
211 | const operationId = op.operationId || `${method}-${path}`;
212 |
213 | if (
214 | input.operationId === operationId ||
215 | (input.path === path &&
216 | input.method?.toLowerCase() === method.toLowerCase())
217 | ) {
218 | targetOperation = op;
219 | targetPath = path;
220 | targetMethod = method;
221 | break;
222 | }
223 | }
224 | if (targetOperation) break;
225 | }
226 |
227 | if (!targetOperation) {
228 | return {
229 | content: [
230 | {
231 | type: "text",
232 | text: `Endpoint not found. Use list_endpoints to see available endpoints.`,
233 | },
234 | ],
235 | };
236 | }
237 |
238 | // Extract parameter information
239 | const parameters = (targetOperation.parameters || []).map(
240 | (param: any) => ({
241 | name: param.name,
242 | in: param.in,
243 | required: param.required || false,
244 | type: param.schema?.type || param.type,
245 | description: param.description || "",
246 | example: param.example || param.schema?.example,
247 | })
248 | );
249 |
250 | // Extract request body schema
251 | let requestBody = null;
252 | if (targetOperation.requestBody) {
253 | const rb = targetOperation.requestBody;
254 | const content = rb.content;
255 | if (content) {
256 | requestBody = Object.keys(content).map((mediaType) => ({
257 | mediaType,
258 | schema: content[mediaType].schema,
259 | required: rb.required || false,
260 | }));
261 | }
262 | }
263 |
264 | // Extract response schemas
265 | const responses = Object.entries(targetOperation.responses || {}).map(
266 | ([code, resp]: [string, any]) => ({
267 | statusCode: code,
268 | description: resp.description || "",
269 | schema: resp.content
270 | ? Object.keys(resp.content).map((mt) => ({
271 | mediaType: mt,
272 | schema: resp.content[mt].schema,
273 | }))
274 | : null,
275 | })
276 | );
277 |
278 | return {
279 | content: [
280 | {
281 | type: "text",
282 | text: JSON.stringify(
283 | {
284 | operationId:
285 | targetOperation.operationId ||
286 | `${targetMethod}-${targetPath}`,
287 | method: targetMethod.toUpperCase(),
288 | path: targetPath,
289 | summary: targetOperation.summary || "",
290 | description: targetOperation.description || "",
291 | tags: targetOperation.tags || [],
292 | deprecated: targetOperation.deprecated || false,
293 | parameters,
294 | requestBody,
295 | responses,
296 | },
297 | null,
298 | 2
299 | ),
300 | },
301 | ],
302 | };
303 | }
304 | );
305 |
306 | // Tool 3: Search endpoints by keyword
307 | this.mcpServer.tool(
308 | "search_endpoints",
309 | "Search API endpoints by keyword in path, summary, description, or tags",
310 | {
311 | input: z.object({
312 | query: z
313 | .string()
314 | .describe(
315 | "Search term to look for in endpoint paths, summaries, descriptions, or tags"
316 | ),
317 | limit: z
318 | .number()
319 | .optional()
320 | .default(20)
321 | .describe("Maximum number of results to return"),
322 | }),
323 | },
324 | async ({ input }) => {
325 | const results = [];
326 | const query = input.query.toLowerCase();
327 |
328 | for (const [path, pathItem] of Object.entries(paths)) {
329 | if (!pathItem) continue;
330 |
331 | for (const [method, operation] of Object.entries(pathItem as any)) {
332 | if (method === "$ref" || !operation) continue;
333 |
334 | const op = operation as any;
335 | const operationId = op.operationId || `${method}-${path}`;
336 |
337 | // Search in various fields
338 | const searchText = [
339 | path,
340 | op.summary || "",
341 | op.description || "",
342 | ...(op.tags || []),
343 | operationId,
344 | ]
345 | .join(" ")
346 | .toLowerCase();
347 |
348 | if (searchText.includes(query)) {
349 | results.push({
350 | operationId,
351 | method: method.toUpperCase(),
352 | path,
353 | summary: op.summary || "",
354 | description: op.description || "",
355 | tags: op.tags || [],
356 | });
357 | }
358 |
359 | if (results.length >= input.limit) break;
360 | }
361 | if (results.length >= input.limit) break;
362 | }
363 |
364 | return {
365 | content: [
366 | {
367 | type: "text",
368 | text: JSON.stringify(
369 | {
370 | query: input.query,
371 | total: results.length,
372 | results,
373 | },
374 | null,
375 | 2
376 | ),
377 | },
378 | ],
379 | };
380 | }
381 | );
382 |
383 | // Tool 4: Make API call
384 | this.mcpServer.tool(
385 | "make_api_call",
386 | "Make an API call to any endpoint with the specified parameters and authentication",
387 | {
388 | input: z.object({
389 | operationId: z
390 | .string()
391 | .optional()
392 | .describe("The operation ID of the endpoint"),
393 | path: z
394 | .string()
395 | .optional()
396 | .describe("The API path (alternative to operationId)"),
397 | method: z
398 | .string()
399 | .optional()
400 | .describe("The HTTP method (required if using path)"),
401 | parameters: z
402 | .record(z.any())
403 | .optional()
404 | .describe("Query parameters, path parameters, or form data"),
405 | body: z
406 | .any()
407 | .optional()
408 | .describe("Request body (for POST, PUT, PATCH requests)"),
409 | auth: z
410 | .object({
411 | type: z
412 | .enum(["none", "basic", "bearer", "apiKey", "oauth2"])
413 | .default("none"),
414 | username: z.string().optional(),
415 | password: z.string().optional(),
416 | token: z.string().optional(),
417 | apiKey: z.string().optional(),
418 | apiKeyName: z.string().optional(),
419 | apiKeyIn: z.enum(["header", "query"]).optional(),
420 | })
421 | .optional()
422 | .describe("Authentication configuration"),
423 | }),
424 | },
425 | async ({ input }) => {
426 | // Find the operation
427 | let targetOperation = null;
428 | let targetPath = "";
429 | let targetMethod = "";
430 |
431 | for (const [path, pathItem] of Object.entries(paths)) {
432 | if (!pathItem) continue;
433 |
434 | for (const [method, operation] of Object.entries(pathItem as any)) {
435 | if (method === "$ref" || !operation) continue;
436 |
437 | const op = operation as any;
438 | const operationId = op.operationId || `${method}-${path}`;
439 |
440 | if (
441 | input.operationId === operationId ||
442 | (input.path === path &&
443 | input.method?.toLowerCase() === method.toLowerCase())
444 | ) {
445 | targetOperation = op;
446 | targetPath = path;
447 | targetMethod = method;
448 | break;
449 | }
450 | }
451 | if (targetOperation) break;
452 | }
453 |
454 | if (!targetOperation) {
455 | return {
456 | content: [
457 | {
458 | type: "text",
459 | text: `Endpoint not found. Use list_endpoints to see available endpoints.`,
460 | },
461 | ],
462 | };
463 | }
464 |
465 | try {
466 | const params = input.parameters || {};
467 | let url = this.apiBaseUrl + targetPath;
468 |
469 | // Handle path parameters
470 | const pathParams = new Set();
471 | targetPath.split("/").forEach((segment) => {
472 | if (segment.startsWith("{") && segment.endsWith("}")) {
473 | pathParams.add(segment.slice(1, -1));
474 | }
475 | });
476 |
477 | Object.entries(params).forEach(([key, value]) => {
478 | if (pathParams.has(key)) {
479 | url = url.replace(`{${key}}`, encodeURIComponent(String(value)));
480 | }
481 | });
482 |
483 | // Separate query parameters
484 | const queryParams = Object.entries(params)
485 | .filter(([key]) => !pathParams.has(key))
486 | .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
487 |
488 | const headers = this.getAuthHeaders(
489 | input.auth?.type !== "none" ? (input.auth as AuthConfig) : undefined
490 | );
491 | const authQueryParams = this.getAuthQueryParams(
492 | input.auth?.type !== "none" ? (input.auth as AuthConfig) : undefined
493 | );
494 |
495 | const response = await axios({
496 | method: targetMethod as string,
497 | url: url,
498 | headers,
499 | data: input.body,
500 | params: { ...queryParams, ...authQueryParams },
501 | });
502 |
503 | return {
504 | content: [
505 | { type: "text", text: JSON.stringify(response.data, null, 2) },
506 | { type: "text", text: `HTTP Status Code: ${response.status}` },
507 | ],
508 | };
509 | } catch (error) {
510 | console.error(`Error in API call:`, error);
511 | if (axios.isAxiosError(error) && error.response) {
512 | return {
513 | content: [
514 | {
515 | type: "text",
516 | text: `Error ${error.response.status}: ${JSON.stringify(
517 | error.response.data,
518 | null,
519 | 2
520 | )}`,
521 | },
522 | ],
523 | };
524 | }
525 | return {
526 | content: [{ type: "text", text: `Error: ${error}` }],
527 | };
528 | }
529 | }
530 | );
531 |
532 | console.debug(
533 | "Successfully registered 4 strategic tools for API navigation"
534 | );
535 | }
536 |
537 | getServer() {
538 | return this.mcpServer;
539 | }
540 | }
541 |
```
--------------------------------------------------------------------------------
/src/postman-mcp-simple.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2 | import { z } from "zod";
3 | import axios from "axios";
4 | import {
5 | Collection,
6 | Request as PostmanRequest,
7 | Item,
8 | ItemGroup,
9 | } from "postman-collection";
10 | import { Request, Response } from "express";
11 | import { AuthConfig, ToolInput } from "./types.js";
12 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
13 |
14 | let transport: SSEServerTransport | null = null;
15 |
16 | export class SimplePostmanMcpServer {
17 | private mcpServer: McpServer;
18 | private collection: Collection | null = null;
19 | private environment: Record<string, any> = {};
20 | private defaultAuth: AuthConfig | undefined;
21 | private requests: Array<{
22 | id: string;
23 | name: string;
24 | method: string;
25 | url: string;
26 | description: string;
27 | folder: string;
28 | request: PostmanRequest;
29 | }> = [];
30 |
31 | constructor(defaultAuth?: AuthConfig) {
32 | if (process.env.NODE_ENV !== "production") {
33 | console.debug("SimplePostmanMcpServer constructor", defaultAuth);
34 | }
35 | this.defaultAuth = defaultAuth;
36 | this.mcpServer = new McpServer({
37 | name: "Simple Postman Collection MCP Server",
38 | version: "1.0.0",
39 | });
40 | }
41 |
42 | private getAuthHeaders(auth?: AuthConfig): Record<string, string> {
43 | const authConfig = auth || this.defaultAuth;
44 | if (!authConfig) return {};
45 |
46 | switch (authConfig.type) {
47 | case "basic":
48 | if (authConfig.username && authConfig.password) {
49 | const credentials = Buffer.from(
50 | `${authConfig.username}:${authConfig.password}`
51 | ).toString("base64");
52 | return { Authorization: `Basic ${credentials}` };
53 | }
54 | break;
55 | case "bearer":
56 | if (authConfig.token) {
57 | return { Authorization: `Bearer ${authConfig.token}` };
58 | }
59 | break;
60 | case "apiKey":
61 | if (
62 | authConfig.apiKey &&
63 | authConfig.apiKeyName &&
64 | authConfig.apiKeyIn === "header"
65 | ) {
66 | return { [authConfig.apiKeyName]: authConfig.apiKey };
67 | }
68 | break;
69 | case "oauth2":
70 | if (authConfig.token) {
71 | return { Authorization: `Bearer ${authConfig.token}` };
72 | }
73 | break;
74 | }
75 | return {};
76 | }
77 |
78 | private getAuthQueryParams(auth?: AuthConfig): Record<string, string> {
79 | const authConfig = auth || this.defaultAuth;
80 | if (!authConfig) return {};
81 |
82 | if (
83 | authConfig.type === "apiKey" &&
84 | authConfig.apiKey &&
85 | authConfig.apiKeyName &&
86 | authConfig.apiKeyIn === "query"
87 | ) {
88 | return { [authConfig.apiKeyName]: authConfig.apiKey };
89 | }
90 |
91 | return {};
92 | }
93 |
94 | private createAuthSchema(): z.ZodType<any> {
95 | return z
96 | .object({
97 | type: z
98 | .enum(["none", "basic", "bearer", "apiKey", "oauth2"])
99 | .default("none"),
100 | username: z.string().optional(),
101 | password: z.string().optional(),
102 | token: z.string().optional(),
103 | apiKey: z.string().optional(),
104 | apiKeyName: z.string().optional(),
105 | apiKeyIn: z.enum(["header", "query"]).optional(),
106 | })
107 | .describe("Authentication configuration for the request");
108 | }
109 |
110 | async loadCollection(collectionUrlOrFile: string) {
111 | if (process.env.NODE_ENV !== "production") {
112 | console.debug("Loading Postman collection from:", collectionUrlOrFile);
113 | }
114 | try {
115 | let collectionData: any;
116 |
117 | if (collectionUrlOrFile.startsWith("http")) {
118 | const response = await axios.get(collectionUrlOrFile);
119 | collectionData = response.data;
120 | } else {
121 | const fs = await import("fs/promises");
122 | const fileContent = await fs.readFile(collectionUrlOrFile, "utf-8");
123 | collectionData = JSON.parse(fileContent);
124 | }
125 |
126 | this.collection = new Collection(collectionData);
127 |
128 | // Get collection info safely
129 | const info = {
130 | name:
131 | (this.collection as any).name ||
132 | collectionData.info?.name ||
133 | "Postman Collection",
134 | description:
135 | (this.collection as any).description ||
136 | collectionData.info?.description ||
137 | "",
138 | version:
139 | (this.collection as any).version ||
140 | collectionData.info?.version ||
141 | "1.0.0",
142 | };
143 |
144 | if (process.env.NODE_ENV !== "production") {
145 | console.debug("Loaded Postman collection:", {
146 | name: info.name,
147 | description:
148 | typeof info.description === "string"
149 | ? info.description.substring(0, 100) + "..."
150 | : "",
151 | });
152 | }
153 |
154 | // Update server name with collection info
155 | this.mcpServer = new McpServer({
156 | name:
157 | `${info.name} - Simple Explorer` ||
158 | "Simple Postman Collection Server",
159 | version: info.version || "1.0.0",
160 | description: `Simplified explorer for ${info.name}` || undefined,
161 | });
162 |
163 | // Parse all requests for the strategic tools
164 | this.parseAllRequests();
165 |
166 | await this.registerStrategicTools();
167 | } catch (error) {
168 | console.error("Failed to load Postman collection:", error);
169 | throw error;
170 | }
171 | }
172 |
173 | async loadEnvironment(environmentUrlOrFile: string) {
174 | if (process.env.NODE_ENV !== "production") {
175 | console.debug("Loading Postman environment from:", environmentUrlOrFile);
176 | }
177 | try {
178 | let environmentData: any;
179 |
180 | if (environmentUrlOrFile.startsWith("http")) {
181 | const response = await axios.get(environmentUrlOrFile);
182 | environmentData = response.data;
183 | } else {
184 | const fs = await import("fs/promises");
185 | const fileContent = await fs.readFile(environmentUrlOrFile, "utf-8");
186 | environmentData = JSON.parse(fileContent);
187 | }
188 |
189 | // Parse environment variables
190 | if (environmentData.values) {
191 | for (const variable of environmentData.values) {
192 | this.environment[variable.key] = variable.value;
193 | }
194 | }
195 |
196 | if (process.env.NODE_ENV !== "production") {
197 | console.debug(
198 | "Loaded environment variables:",
199 | Object.keys(this.environment)
200 | );
201 | }
202 | } catch (error) {
203 | console.error("Failed to load Postman environment:", error);
204 | throw error;
205 | }
206 | }
207 |
208 | private parseAllRequests() {
209 | if (!this.collection) return;
210 |
211 | this.requests = [];
212 |
213 | const parseItem = (
214 | item: Item | ItemGroup<Item>,
215 | folderPath: string = ""
216 | ) => {
217 | if (item instanceof Item && item.request) {
218 | const request = item.request;
219 |
220 | // Handle description safely
221 | let description = "";
222 | if (item.request.description) {
223 | if (typeof item.request.description === "string") {
224 | description = item.request.description;
225 | } else if (
226 | typeof item.request.description === "object" &&
227 | "content" in item.request.description
228 | ) {
229 | description = (item.request.description as any).content;
230 | }
231 | }
232 |
233 | this.requests.push({
234 | id: `${folderPath}${item.name}`
235 | .replace(/[^a-zA-Z0-9_]/g, "_")
236 | .toLowerCase(),
237 | name: item.name || "Unnamed Request",
238 | method: request.method || "GET",
239 | url: request.url?.toString() || "",
240 | description,
241 | folder: folderPath,
242 | request,
243 | });
244 | } else if (item instanceof ItemGroup) {
245 | // Handle folders recursively
246 | const newFolderPath = folderPath
247 | ? `${folderPath}/${item.name}`
248 | : item.name;
249 | item.items.each((subItem: Item | ItemGroup<Item>) => {
250 | parseItem(subItem, newFolderPath);
251 | });
252 | }
253 | };
254 |
255 | // Parse all items
256 | this.collection.items.each((item: Item | ItemGroup<Item>) => {
257 | parseItem(item);
258 | });
259 |
260 | console.log(
261 | `✅ Parsed ${this.requests.length} requests from Postman collection`
262 | );
263 | }
264 |
265 | private resolveVariables(text: string): string {
266 | if (!text) return text;
267 |
268 | // Replace {{variableName}} with actual values
269 | return text.replace(/\{\{(\w+)\}\}/g, (match, variableName) => {
270 | return this.environment[variableName] || match;
271 | });
272 | }
273 |
274 | private async registerStrategicTools() {
275 | // Tool 1: List all requests
276 | this.mcpServer.tool(
277 | "list_requests",
278 | "List all available requests in the Postman collection with basic information",
279 | {
280 | input: z.object({
281 | method: z
282 | .string()
283 | .optional()
284 | .describe("Filter by HTTP method (GET, POST, PUT, DELETE, etc.)"),
285 | folder: z.string().optional().describe("Filter by folder/path"),
286 | limit: z
287 | .number()
288 | .optional()
289 | .default(50)
290 | .describe("Maximum number of requests to return"),
291 | }),
292 | },
293 | async ({ input }) => {
294 | let filteredRequests = this.requests;
295 |
296 | // Apply filters
297 | if (input.method) {
298 | filteredRequests = filteredRequests.filter(
299 | (req) => req.method.toLowerCase() === input.method!.toLowerCase()
300 | );
301 | }
302 |
303 | if (input.folder) {
304 | filteredRequests = filteredRequests.filter((req) =>
305 | req.folder.toLowerCase().includes(input.folder!.toLowerCase())
306 | );
307 | }
308 |
309 | // Limit results
310 | const limitedRequests = filteredRequests.slice(0, input.limit);
311 |
312 | return {
313 | content: [
314 | {
315 | type: "text",
316 | text: JSON.stringify(
317 | {
318 | total: limitedRequests.length,
319 | requests: limitedRequests.map((req) => ({
320 | id: req.id,
321 | name: req.name,
322 | method: req.method,
323 | url: req.url,
324 | folder: req.folder,
325 | description:
326 | req.description.substring(0, 100) +
327 | (req.description.length > 100 ? "..." : ""),
328 | })),
329 | },
330 | null,
331 | 2
332 | ),
333 | },
334 | ],
335 | };
336 | }
337 | );
338 |
339 | // Tool 2: Get detailed information about a specific request
340 | this.mcpServer.tool(
341 | "get_request_details",
342 | "Get detailed information about a specific request including parameters, headers, and body structure",
343 | {
344 | input: z.object({
345 | requestId: z.string().describe("The ID of the request"),
346 | name: z
347 | .string()
348 | .optional()
349 | .describe("The name of the request (alternative to ID)"),
350 | }),
351 | },
352 | async ({ input }) => {
353 | let targetRequest = null;
354 |
355 | // Find the request by ID or name
356 | for (const req of this.requests) {
357 | if (
358 | req.id === input.requestId ||
359 | req.name.toLowerCase() === input.name?.toLowerCase()
360 | ) {
361 | targetRequest = req;
362 | break;
363 | }
364 | }
365 |
366 | if (!targetRequest) {
367 | return {
368 | content: [
369 | {
370 | type: "text",
371 | text: `Request not found. Use list_requests to see available requests.`,
372 | },
373 | ],
374 | };
375 | }
376 |
377 | const request = targetRequest.request;
378 |
379 | // Extract parameters information
380 | const parameters = {
381 | query: [] as any[],
382 | path: [] as any[],
383 | headers: [] as any[],
384 | };
385 |
386 | // Query parameters
387 | if (request.url && request.url.query) {
388 | request.url.query.each((param: any) => {
389 | if (param.key && !param.disabled) {
390 | parameters.query.push({
391 | name: param.key,
392 | value: param.value || "",
393 | description: param.description || "",
394 | });
395 | }
396 | });
397 | }
398 |
399 | // Path variables
400 | if (request.url && request.url.variables) {
401 | request.url.variables.each((variable: any) => {
402 | if (variable.key) {
403 | parameters.path.push({
404 | name: variable.key,
405 | value: variable.value || "",
406 | description: variable.description || "",
407 | });
408 | }
409 | });
410 | }
411 |
412 | // Headers
413 | if (request.headers) {
414 | request.headers.each((header: any) => {
415 | if (header.key && !header.disabled) {
416 | parameters.headers.push({
417 | name: header.key,
418 | value: header.value || "",
419 | description: header.description || "",
420 | });
421 | }
422 | });
423 | }
424 |
425 | // Request body info
426 | let bodyInfo: any = null;
427 | if (
428 | request.body &&
429 | ["POST", "PUT", "PATCH"].includes(request.method || "")
430 | ) {
431 | bodyInfo = {
432 | mode: request.body.mode,
433 | description: "Request body based on the collection definition",
434 | } as any;
435 |
436 | if (request.body.mode === "raw") {
437 | bodyInfo.example = request.body.raw || "";
438 | } else if (request.body.mode === "formdata") {
439 | bodyInfo.formFields = [];
440 | if (request.body.formdata) {
441 | request.body.formdata.each((field: any) => {
442 | if (field.key && !field.disabled) {
443 | bodyInfo.formFields.push({
444 | name: field.key,
445 | type: field.type || "text",
446 | value: field.value || "",
447 | description: field.description || "",
448 | });
449 | }
450 | });
451 | }
452 | }
453 | }
454 |
455 | return {
456 | content: [
457 | {
458 | type: "text",
459 | text: JSON.stringify(
460 | {
461 | id: targetRequest.id,
462 | name: targetRequest.name,
463 | method: targetRequest.method,
464 | url: targetRequest.url,
465 | folder: targetRequest.folder,
466 | description: targetRequest.description,
467 | parameters,
468 | body: bodyInfo,
469 | auth: "Use the auth parameter in make_request to provide authentication",
470 | },
471 | null,
472 | 2
473 | ),
474 | },
475 | ],
476 | };
477 | }
478 | );
479 |
480 | // Tool 3: Search requests by keyword
481 | this.mcpServer.tool(
482 | "search_requests",
483 | "Search requests by keyword in name, description, URL, or folder",
484 | {
485 | input: z.object({
486 | query: z
487 | .string()
488 | .describe(
489 | "Search term to look for in request names, descriptions, URLs, or folders"
490 | ),
491 | limit: z
492 | .number()
493 | .optional()
494 | .default(20)
495 | .describe("Maximum number of results to return"),
496 | }),
497 | },
498 | async ({ input }) => {
499 | const query = input.query.toLowerCase();
500 | const results = [];
501 |
502 | for (const req of this.requests) {
503 | const searchText = [
504 | req.name,
505 | req.description,
506 | req.url,
507 | req.folder,
508 | req.method,
509 | ]
510 | .join(" ")
511 | .toLowerCase();
512 |
513 | if (searchText.includes(query)) {
514 | results.push({
515 | id: req.id,
516 | name: req.name,
517 | method: req.method,
518 | url: req.url,
519 | folder: req.folder,
520 | description:
521 | req.description.substring(0, 100) +
522 | (req.description.length > 100 ? "..." : ""),
523 | relevance: this.calculateRelevance(query, searchText),
524 | });
525 |
526 | if (results.length >= input.limit) break;
527 | }
528 | }
529 |
530 | // Sort by relevance
531 | results.sort((a, b) => b.relevance - a.relevance);
532 |
533 | return {
534 | content: [
535 | {
536 | type: "text",
537 | text: JSON.stringify(
538 | {
539 | query: input.query,
540 | total: results.length,
541 | results: results.map((r) => ({ ...r, relevance: undefined })), // Remove relevance from output
542 | },
543 | null,
544 | 2
545 | ),
546 | },
547 | ],
548 | };
549 | }
550 | );
551 |
552 | // Tool 4: Make request
553 | this.mcpServer.tool(
554 | "make_request",
555 | "Execute any request from the Postman collection with the specified parameters and authentication",
556 | {
557 | input: z.object({
558 | requestId: z.string().optional().describe("The ID of the request"),
559 | name: z
560 | .string()
561 | .optional()
562 | .describe("The name of the request (alternative to ID)"),
563 | parameters: z
564 | .record(z.any())
565 | .optional()
566 | .describe(
567 | "Query parameters, path parameters, headers, or form data"
568 | ),
569 | body: z
570 | .any()
571 | .optional()
572 | .describe("Request body (for POST, PUT, PATCH requests)"),
573 | auth: this.createAuthSchema()
574 | .optional()
575 | .describe("Authentication configuration"),
576 | }),
577 | },
578 | async ({ input }) => {
579 | // Find the request
580 | let targetRequest = null;
581 |
582 | for (const req of this.requests) {
583 | if (
584 | req.id === input.requestId ||
585 | req.name.toLowerCase() === input.name?.toLowerCase()
586 | ) {
587 | targetRequest = req;
588 | break;
589 | }
590 | }
591 |
592 | if (!targetRequest) {
593 | return {
594 | content: [
595 | {
596 | type: "text",
597 | text: `Request not found. Use list_requests to see available requests.`,
598 | },
599 | ],
600 | };
601 | }
602 |
603 | try {
604 | const result = await this.executeRequest(
605 | targetRequest.request,
606 | input.parameters || {},
607 | input.body,
608 | input.auth
609 | );
610 | return {
611 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
612 | };
613 | } catch (error) {
614 | return {
615 | content: [
616 | {
617 | type: "text",
618 | text: `Error: ${
619 | error instanceof Error ? error.message : String(error)
620 | }`,
621 | },
622 | ],
623 | };
624 | }
625 | }
626 | );
627 |
628 | console.log(
629 | "✅ Successfully registered 4 strategic tools for Postman collection exploration"
630 | );
631 | }
632 |
633 | private calculateRelevance(query: string, text: string): number {
634 | const queryWords = query.split(" ");
635 | let score = 0;
636 |
637 | queryWords.forEach((word) => {
638 | if (text.includes(word)) {
639 | score += 1;
640 | // Bonus for exact word matches
641 | if (
642 | text.includes(` ${word} `) ||
643 | text.startsWith(word) ||
644 | text.endsWith(word)
645 | ) {
646 | score += 0.5;
647 | }
648 | }
649 | });
650 |
651 | return score;
652 | }
653 |
654 | private async executeRequest(
655 | request: PostmanRequest,
656 | parameters: Record<string, any>,
657 | body?: any,
658 | auth?: AuthConfig
659 | ): Promise<any> {
660 | // Build URL
661 | let url = request.url?.toString() || "";
662 | url = this.resolveVariables(url);
663 |
664 | // Replace path variables
665 | Object.keys(parameters).forEach((key) => {
666 | const value = parameters[key];
667 | if (value !== undefined) {
668 | // Try different variable formats
669 | url = url.replace(`:${key}`, encodeURIComponent(String(value)));
670 | url = url.replace(`{{${key}}}`, encodeURIComponent(String(value)));
671 | url = url.replace(`{${key}}`, encodeURIComponent(String(value)));
672 | }
673 | });
674 |
675 | // Build query parameters
676 | const queryParams = this.getAuthQueryParams(auth);
677 | Object.keys(parameters).forEach((key) => {
678 | const value = parameters[key];
679 | if (
680 | value !== undefined &&
681 | !url.includes(`:${key}`) &&
682 | !url.includes(`{{${key}}}`)
683 | ) {
684 | queryParams[key] = value;
685 | }
686 | });
687 |
688 | // Build headers
689 | const headers = this.getAuthHeaders(auth);
690 |
691 | // Add any headers from parameters
692 | Object.keys(parameters).forEach((key) => {
693 | const value = parameters[key];
694 | if (value !== undefined && key.toLowerCase().includes("header")) {
695 | headers[key] = value;
696 | }
697 | });
698 |
699 | // Add default content type for requests with body
700 | if (body && !headers["Content-Type"]) {
701 | headers["Content-Type"] = "application/json";
702 | }
703 |
704 | // Prepare request configuration
705 | const config: any = {
706 | method: request.method || "GET",
707 | url,
708 | headers,
709 | params: queryParams,
710 | };
711 |
712 | // Add body if present
713 | if (body) {
714 | if (typeof body === "string") {
715 | config.data = body;
716 | } else {
717 | config.data = JSON.stringify(body);
718 | }
719 | }
720 |
721 | if (process.env.NODE_ENV !== "production") {
722 | console.debug("Executing request:", {
723 | method: config.method,
724 | url: config.url,
725 | headers: Object.keys(config.headers),
726 | hasBody: !!config.data,
727 | });
728 | }
729 |
730 | try {
731 | const response = await axios(config);
732 | return {
733 | status: response.status,
734 | statusText: response.statusText,
735 | headers: response.headers,
736 | data: response.data,
737 | };
738 | } catch (error: any) {
739 | if (error.response) {
740 | return {
741 | status: error.response.status,
742 | statusText: error.response.statusText,
743 | headers: error.response.headers,
744 | data: error.response.data,
745 | error: true,
746 | };
747 | }
748 | throw error;
749 | }
750 | }
751 |
752 | getServer() {
753 | return this.mcpServer;
754 | }
755 |
756 | handleSSE(res: Response) {
757 | if (!transport) {
758 | transport = new SSEServerTransport("/messages", res);
759 | }
760 | this.mcpServer.connect(transport);
761 | }
762 |
763 | handleMessage(req: Request, res: Response) {
764 | this.mcpServer.connect(transport!);
765 | }
766 | }
767 |
```