# 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:
--------------------------------------------------------------------------------
```
# Server Configuration
PORT=3000
# API Authentication
API_USERNAME=
API_PASSWORD=
API_TOKEN=
# Default API Configuration
DEFAULT_API_BASE_URL=
DEFAULT_SWAGGER_URL=
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Dependencies
node_modules/
yarn-debug.log*
yarn-error.log*
# Build output
dist/
build/
# Environment variables
.env
.env.local
.env.*.local
# IDE and editor files
.idea/
.vscode/
*.swp
*.swo
.DS_Store
# Test coverage
coverage/
# Logs
logs/
*.log
npm-debug.log*
# Local test files
ns-openapi.json
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Swagger/Postman MCP Server
Server that ingests and serves Swagger/OpenAPI specifications and Postman collections as MCP (Model Context Protocol) tools using a **simplified strategic approach**.
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:
```bash
Example prompt:
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)
```
## Features
- **Strategic Tool Approach**: Only 4 tools instead of hundreds for better AI agent performance
- **OpenAPI/Swagger Support**: Load OpenAPI 2.0/3.0 specifications from URLs or local files
- **Postman Collection Support**: Load Postman collection JSON files from URLs or local files
- **Environment Variables**: Support for Postman environment files
- **Authentication**: Multiple authentication methods (Basic, Bearer, API Key, OAuth2)
- **Dynamic API Discovery**: Tools for listing, searching, and getting details about API endpoints
- **Request Execution**: Execute API requests with proper parameter handling and authentication
## Security
This is a personal server!! Do not expose it to the public internet.
If the underlying API requires authentication, you should not expose the MCP server to the public internet.
## TODO
- secrets - the MCP server should be able to use secrets from the user to authenticate requests to the API
- Comprehensive test suite
## Prerequisites
- Node.js (v18 or higher)
- Yarn package manager
- TypeScript
## Installation
```bash
# Clone the repository
git clone <repository-url>
cd swag-mcp
# Install dependencies
npm install
# or
yarn install
# Build the project
npm run build
# or
yarn build
# Make the start script executable (Linux/macOS)
chmod +x start-mcp.sh
```
### Quick Setup for Cursor
1. **Clone and build** (commands above)
2. **Configure** your `config.json` with your API details
3. **Update paths**: Edit `start-mcp.sh` and change the `cd` path to your installation directory
4. **Add to Cursor**: Edit `~/.cursor/mcp.json` and add:
```json
{
"mcpServers": {
"postman-swagger-api": {
"command": "/full/path/to/your/swag-mcp/start-mcp.sh"
}
}
}
```
5. **Restart Cursor** and start using the 4 strategic MCP tools!
## Configuration
The server uses a `config.json` file for configuration. You can specify either OpenAPI/Swagger specifications or Postman collections.
### OpenAPI/Swagger Configuration
```json
{
"api": {
"type": "openapi",
"openapi": {
"url": "https://petstore.swagger.io/v2/swagger.json",
"apiBaseUrl": "https://petstore.swagger.io/v2",
"defaultAuth": {
"type": "apiKey",
"apiKey": "special-key",
"apiKeyName": "api_key",
"apiKeyIn": "header"
}
}
},
"log": {
"level": "info"
}
}
```
### Postman Collection Configuration
```json
{
"api": {
"type": "postman",
"postman": {
"collectionUrl": "https://www.postman.com/collections/your-collection-id",
"collectionFile": "./examples/postman-collection.json",
"environmentUrl": "https://www.postman.com/environments/your-environment-id",
"environmentFile": "./examples/postman-environment.json",
"defaultAuth": {
"type": "bearer",
"token": "your-api-token-here"
}
}
},
"log": {
"level": "info"
}
}
```
### Configuration Options
#### API Configuration
- `api.type`: Either `"openapi"` or `"postman"`
- `api.openapi`: OpenAPI/Swagger specific configuration
- `url`: URL to the OpenAPI specification
- `apiBaseUrl`: Base URL for API requests
- `defaultAuth`: Default authentication configuration
- `api.postman`: Postman specific configuration
- `collectionUrl`: URL to the Postman collection (optional)
- `collectionFile`: Path to local Postman collection file (optional)
- `environmentUrl`: URL to the Postman environment (optional)
- `environmentFile`: Path to local Postman environment file (optional)
- `defaultAuth`: Default authentication configuration
#### Authentication Configuration
- `type`: Authentication type (`"basic"`, `"bearer"`, `"apiKey"`, `"oauth2"`)
- `username`: Username (for basic auth)
- `password`: Password (for basic auth)
- `token`: Token (for bearer/oauth2 auth)
- `apiKey`: API key value
- `apiKeyName`: API key parameter name
- `apiKeyIn`: Where to send API key (`"header"` or `"query"`)
#### Logging Configuration
- `log.level`: Logging level (`"debug"`, `"info"`, `"warn"`, `"error"`)
## Usage
### Starting the MCP Server
The server runs via stdio transport for MCP connections:
```bash
# Start the simplified MCP server via stdio
./start-mcp.sh
# Or directly with node
node dist/simple-stdio.js
# For development with auto-reload
npm run dev:simple
# or
yarn dev:simple
```
### MCP Integration
This server uses stdio transport and is designed to be used with MCP clients like Claude Desktop or Cursor.
## Installing in Cursor
To use this MCP server with Cursor, you need to add it to your Cursor MCP configuration:
### 1. Locate your Cursor MCP configuration file
The configuration file is located at:
- **Linux/macOS**: `~/.cursor/mcp.json`
- **Windows**: `%APPDATA%\.cursor\mcp.json`
### 2. Add the MCP server configuration
Edit your `mcp.json` file to include this server:
```json
{
"mcpServers": {
"postman-swagger-api": {
"command": "/path/to/your/swag-mcp/start-mcp.sh"
}
}
}
```
**⚠️ Important: Change the path!**
Replace `/path/to/your/swag-mcp/start-mcp.sh` with the actual path to your cloned repository. For example:
- **Linux/macOS**: `"/home/username/Documents/swag-mcp/start-mcp.sh"`
- **Windows**: `"C:\\Users\\username\\Documents\\swag-mcp\\start-mcp.sh"`
### 3. Example complete configuration
```json
{
"mcpServers": {
"supabase": {
"command": "npx",
"args": [
"-y",
"@supabase/mcp-server-supabase@latest",
"--access-token",
"your-supabase-token"
]
},
"postman-swagger-api": {
"command": "/home/username/Documents/swag-mcp/start-mcp.sh"
}
}
}
```
### 4. Restart Cursor
After saving the configuration file, restart Cursor for the changes to take effect.
### 5. Verify installation
In Cursor, you should now have access to the 4 strategic MCP tools:
- `list_requests` - List all available requests
- `get_request_details` - Get detailed request information
- `search_requests` - Search requests by keyword
- `make_request` - Execute any API request
### Troubleshooting
If the MCP server fails to start:
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
2. **Check the path**: Ensure the path in `mcp.json` points to your actual `start-mcp.sh` file
3. **Check permissions**: Make sure `start-mcp.sh` is executable (`chmod +x start-mcp.sh`)
4. **Check build**: Ensure you've run `npm run build` to compile the TypeScript files
5. **Check logs**: Look in Cursor's MCP logs for error messages
### Example Path Updates
If you cloned to `/home/username/Documents/swag-mcp/`, then:
**In `start-mcp.sh`:**
```bash
cd "/home/username/Documents/swag-mcp"
```
**In `~/.cursor/mcp.json`:**
```json
"command": "/home/username/Documents/swag-mcp/start-mcp.sh"
```
## How It Works
### Strategic Tool Approach
Instead of generating hundreds of individual tools for each API endpoint, this server provides **4 strategic tools** that enable dynamic API discovery and interaction:
### OpenAPI/Swagger Mode
**4 Strategic Tools:**
1. **`list_endpoints`** - List all available API endpoints
2. **`get_endpoint_details`** - Get detailed information about specific endpoints
3. **`search_endpoints`** - Search endpoints by keyword
4. **`make_api_call`** - Execute any API call with proper authentication
**Process:**
1. Loads the OpenAPI specification from the configured URL or file
2. Parses the specification to extract API endpoints, parameters, and security schemes
3. Makes endpoint information available through the 4 strategic tools
4. Handles authentication and parameter validation dynamically
5. Executes API requests and returns responses
### Postman Collection Mode
**4 Strategic Tools:**
1. **`list_requests`** - List all available requests in the collection
2. **`get_request_details`** - Get detailed information about specific requests
3. **`search_requests`** - Search requests by keyword
4. **`make_request`** - Execute any request from the collection
**Process:**
1. Loads the Postman collection JSON from the configured URL or file
2. Optionally loads a Postman environment file for variable substitution
3. Parses requests, folders, and nested items in the collection
4. Makes request information available through the 4 strategic tools
5. Handles variable substitution, authentication, and parameter mapping dynamically
6. Executes requests with proper headers, query parameters, and body data
### Benefits of Strategic Tools
- **Better AI Performance**: 4 tools vs hundreds means faster decision making
- **Dynamic Discovery**: AI agents can explore APIs without knowing endpoints beforehand
- **Flexible Interaction**: Any endpoint can be called through `make_api_call`/`make_request`
- **Reduced Overwhelm**: AI agents aren't flooded with tool options
## Strategic Tools Reference
### For OpenAPI/Swagger APIs
1. **`list_endpoints`**
- Lists all available API endpoints with methods and paths
- No parameters required
- Returns: Array of endpoint summaries
2. **`get_endpoint_details`**
- Get detailed information about a specific endpoint
- Parameters: `method` (GET/POST/etc), `path` (/users/{id}/etc)
- Returns: Full endpoint specification with parameters, body schema, responses
3. **`search_endpoints`**
- Search endpoints by keyword in path, summary, or description
- Parameters: `query` (search term)
- Returns: Filtered list of matching endpoints
4. **`make_api_call`**
- Execute an API call to any endpoint
- Parameters: `method`, `path`, `pathParams`, `queryParams`, `headers`, `body`
- Returns: API response with status and data
### For Postman Collections
1. **`list_requests`**
- Lists all available requests in the collection
- No parameters required
- Returns: Array of request summaries
2. **`get_request_details`**
- Get detailed information about a specific request
- Parameters: `requestId` or `requestName`
- Returns: Full request specification
3. **`search_requests`**
- Search requests by keyword
- Parameters: `query` (search term)
- Returns: Filtered list of matching requests
4. **`make_request`**
- Execute any request from the collection
- Parameters: `requestId`, `variables` (for substitution)
- Returns: Request response
### Authentication
The server supports multiple authentication methods:
- **Basic Authentication**: Username/password
- **Bearer Token**: JWT or other bearer tokens
- **API Key**: In headers or query parameters
- **OAuth2**: Bearer token based
Authentication can be configured globally or overridden per request.
## Example Configuration
Your `config.json` should specify either OpenAPI or Postman configuration as shown above.
### Example Postman Collection Structure
```json
{
"info": {
"name": "Sample API Collection",
"description": "A sample Postman collection"
},
"item": [
{
"name": "Get Users",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/users",
"host": ["{{baseUrl}}"],
"path": ["users"]
}
}
}
]
}
```
## Development
```bash
# Install dependencies
npm install
# Run in development mode
npm run dev
# Run tests
npm test
# Build for production
npm run build
```
## License
ISC
## Environment Variables
- `PORT`: Server port (default: 3000)
- `API_USERNAME`: Username for API authentication (fallback)
- `API_PASSWORD`: Password for API authentication (fallback)
- `API_TOKEN`: API token for authentication (fallback)
- `DEFAULT_API_BASE_URL`: Default base URL for API endpoints (fallback)
- `DEFAULT_SWAGGER_URL`: Default Swagger specification URL
```
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
```javascript
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/tests'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};
```
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
```json
{
"api": {
"type": "postman",
"postman": {
"collectionFile": "./ns-openapi.json",
"environmentFile": "./ns-openapi.json",
"defaultAuth": {
"type": "bearer",
"token": "test-token"
}
}
},
"log": {
"level": "debug"
},
"server": {
"port": 9001,
"host": "0.0.0.0"
}
}
```
--------------------------------------------------------------------------------
/start-mcp.sh:
--------------------------------------------------------------------------------
```bash
#!/bin/bash
# ⚠️ IMPORTANT: Update this path to match your installation directory!
# Change this to the full path where you cloned the swag-mcp repository
cd "/path/to/your/swag-mcp"
# Set environment variables
export NODE_ENV=production
# Start the simplified MCP server (only 4 strategic tools instead of 300+)
exec node dist/simple-stdio.js
```
--------------------------------------------------------------------------------
/test-simple.ts:
--------------------------------------------------------------------------------
```typescript
import { SimpleSwaggerMcpServer } from "./src/swagger-mcp-simple.js";
async function test() {
console.log("Testing simplified MCP server...");
const server = new SimpleSwaggerMcpServer("https://api.example.com");
// Test with a simple OpenAPI spec
const simpleSpec = {
openapi: "3.0.0",
info: {
title: "Test API",
version: "1.0.0",
},
paths: {
"/users": {
get: {
operationId: "getUsers",
summary: "Get all users",
responses: {
"200": {
description: "Success",
},
},
},
},
},
};
console.log("✅ SimpleSwaggerMcpServer created successfully");
console.log("This demonstrates the strategic tool approach works!");
}
test().catch(console.error);
```
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
```typescript
export interface SwaggerConfig {
swaggerUrl?: string;
swaggerFile?: string;
apiBaseUrl: string;
auth?: AuthConfig;
}
export interface PostmanConfig {
collectionUrl?: string;
collectionFile?: string;
environmentUrl?: string;
environmentFile?: string;
auth?: AuthConfig;
}
export interface ApiConfig {
type: "openapi" | "postman";
openapi?: SwaggerConfig;
postman?: PostmanConfig;
}
export interface AuthConfig {
type: "basic" | "bearer" | "apiKey" | "oauth2";
username?: string;
password?: string;
token?: string;
apiKey?: string;
apiKeyName?: string;
apiKeyIn?: "header" | "query";
}
export interface ToolInput {
auth?: AuthConfig;
[key: string]: any;
}
export interface SecurityScheme {
type: string;
description?: string;
name?: string;
in?: string;
scheme?: string;
flows?: {
implicit?: {
authorizationUrl: string;
scopes: Record<string, string>;
};
[key: string]: any;
};
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "swag-mcp",
"version": "1.0.0",
"description": "An MCP server that ingests and serves Swagger/OpenAPI specifications and Postman collections",
"main": "dist/simple-stdio.js",
"scripts": {
"start": "node dist/simple-stdio.js",
"start:simple": "node dist/simple-stdio.js",
"dev": "NODE_OPTIONS='--loader ts-node/esm' ts-node src/simple-stdio.ts",
"dev:simple": "NODE_OPTIONS='--loader ts-node/esm' ts-node src/simple-server.ts",
"build": "tsc",
"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",
"watch": "tsc -w",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"keywords": [
"swagger",
"openapi",
"postman",
"collections",
"api",
"documentation",
"mcp",
"mcp-server",
"postman"
],
"author": "",
"license": "ISC",
"dependencies": {
"@apidevtools/swagger-parser": "^10.1.0",
"@modelcontextprotocol/sdk": "^1.7.0",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/postman-collection": "^3.5.10",
"@types/swagger-parser": "^7.0.1",
"axios": "^1.8.3",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"eventsource": "^3.0.5",
"express": "^4.18.3",
"node-fetch": "^3.3.2",
"openapi-types": "^12.1.3",
"postman-collection": "^5.0.2",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typescript": "^5.4.2",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/supertest": "^6.0.2",
"jest": "^29.7.0",
"supertest": "^7.0.0",
"ts-jest": "^29.2.6"
},
"packageManager": "[email protected]+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72"
}
```
--------------------------------------------------------------------------------
/src/simple-server.ts:
--------------------------------------------------------------------------------
```typescript
import { SimpleSwaggerMcpServer } from "./swagger-mcp-simple.js";
import { loadConfig } from "./config.js";
async function main() {
try {
const configPath = process.argv[2] || undefined;
const config = await loadConfig(configPath);
// Use new config structure or fallback to legacy
const openApiConfig =
config.api.type === "openapi" ? config.api.openapi : config.swagger;
if (!openApiConfig) {
throw new Error("No OpenAPI configuration found");
}
console.log("Creating MCP server with config:", {
apiBaseUrl: openApiConfig.apiBaseUrl,
authType: openApiConfig.defaultAuth?.type,
});
const server = new SimpleSwaggerMcpServer(
openApiConfig.apiBaseUrl,
openApiConfig.defaultAuth && openApiConfig.defaultAuth.type
? (openApiConfig.defaultAuth as any)
: undefined
);
// Load the swagger spec
await server.loadSwaggerSpec(openApiConfig.url);
console.log(
"✅ Simple MCP Server successfully initialized with strategic tools!"
);
console.log("Now you have only 4 tools instead of hundreds:");
console.log(" 1. list_endpoints - List all available API endpoints");
console.log(
" 2. get_endpoint_details - Get detailed info about specific endpoints"
);
console.log(" 3. search_endpoints - Search endpoints by keyword");
console.log(" 4. make_api_call - Make actual API calls");
console.log("");
console.log("This approach allows AI agents to:");
console.log(" - Discover APIs dynamically");
console.log(" - Get required parameter info");
console.log(" - Make informed API calls");
console.log(" - Search for relevant endpoints");
return server.getServer();
} catch (error) {
console.error("Failed to initialize MCP server:", error);
process.exit(1);
}
}
main().catch(console.error);
```
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
import fs from "fs/promises";
import path from "path";
// Define auth configuration schema
const AuthConfigSchema = z.object({
type: z.enum(["basic", "bearer", "apiKey", "oauth2"]),
token: z.string().optional(),
username: z.string().optional(),
password: z.string().optional(),
apiKey: z.string().optional(),
apiKeyName: z.string().optional(),
apiKeyIn: z.enum(["header", "query"]).optional(),
});
// Define the configuration schema
export const ConfigSchema = z
.object({
api: z.object({
type: z.enum(["openapi", "postman"]),
openapi: z
.object({
url: z.string().url(),
apiBaseUrl: z.string().url(),
defaultAuth: AuthConfigSchema.optional(),
})
.optional(),
postman: z
.object({
collectionUrl: z.string().url().optional(),
collectionFile: z.string().optional(),
environmentUrl: z.string().url().optional(),
environmentFile: z.string().optional(),
defaultAuth: AuthConfigSchema.optional(),
})
.optional(),
}),
// Keep legacy swagger config for backward compatibility
swagger: z
.object({
url: z.string().url(),
apiBaseUrl: z.string().url(),
defaultAuth: AuthConfigSchema.optional(),
})
.optional(),
log: z.object({
level: z.enum(["debug", "info", "warn", "error"]),
}),
server: z.object({
port: z.number().default(3000),
host: z.string().default("0.0.0.0"),
}),
})
.refine(
(data) => {
// Ensure we have either the new api config or legacy swagger config
if (data.api.type === "openapi" && !data.api.openapi && !data.swagger) {
return false;
}
if (data.api.type === "postman" && !data.api.postman) {
return false;
}
return true;
},
{
message:
"Configuration must include appropriate API settings based on type",
}
);
export type Config = z.infer<typeof ConfigSchema>;
const defaultConfig: Config = {
api: {
type: "openapi",
openapi: {
url: "https://petstore.swagger.io/v2/swagger.json",
apiBaseUrl: "https://petstore.swagger.io/v2",
defaultAuth: {
type: "apiKey",
apiKey: "special-key",
apiKeyName: "api_key",
apiKeyIn: "header",
},
},
},
swagger: {
url: "https://petstore.swagger.io/v2/swagger.json",
apiBaseUrl: "https://petstore.swagger.io/v2",
defaultAuth: {
type: "apiKey",
apiKey: "special-key",
apiKeyName: "api_key",
apiKeyIn: "header",
},
},
log: {
level: "info",
},
server: {
port: 3000,
host: "0.0.0.0",
},
};
export async function loadConfig(configPath?: string): Promise<Config> {
try {
// If no config path provided, create default config file
if (!configPath) {
configPath = path.join(process.cwd(), "config.json");
// Check if config file exists, if not create it with default values
try {
await fs.access(configPath);
} catch {
await fs.writeFile(configPath, JSON.stringify(defaultConfig, null, 2));
console.log(`Created default configuration file at ${configPath}`);
}
}
const configFile = await fs.readFile(configPath, "utf-8");
const config = JSON.parse(configFile);
// Handle legacy config migration
if (config.swagger && !config.api) {
config.api = {
type: "openapi",
openapi: config.swagger,
};
}
return ConfigSchema.parse(config);
} catch (error) {
if (error instanceof z.ZodError) {
console.error("Invalid configuration:", error.errors);
} else {
console.error("Error loading configuration:", error);
}
console.log("Using default configuration");
return defaultConfig;
}
}
```
--------------------------------------------------------------------------------
/src/simple-stdio.ts:
--------------------------------------------------------------------------------
```typescript
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { loadConfig } from "./config.js";
import { SimpleSwaggerMcpServer } from "./swagger-mcp-simple.js";
import { SimplePostmanMcpServer } from "./postman-mcp-simple.js";
async function main() {
try {
console.error(`[SIMPLE-STDIO] Starting simplified MCP server via stdio...`);
console.error(`[SIMPLE-STDIO] Process ID: ${process.pid}`);
console.error(`[SIMPLE-STDIO] Working directory: ${process.cwd()}`);
// Load configuration
console.error(`[SIMPLE-STDIO] Loading configuration...`);
const config = await loadConfig();
let mcpServer: SimpleSwaggerMcpServer | SimplePostmanMcpServer;
// Create and initialize MCP server based on configuration
if (config.api.type === "postman") {
if (!config.api.postman) {
throw new Error(
'Postman configuration is required when api.type is "postman"'
);
}
console.error(
"[SIMPLE-STDIO] Creating simplified Postman MCP server instance..."
);
console.error(
"✅ Using simplified Postman explorer with only 4 strategic tools!"
);
mcpServer = new SimplePostmanMcpServer(config.api.postman.defaultAuth);
// Load collection
const collectionSource =
config.api.postman.collectionUrl || config.api.postman.collectionFile;
if (!collectionSource) {
throw new Error(
"Either collectionUrl or collectionFile must be specified for Postman configuration"
);
}
console.error("[SIMPLE-STDIO] Loading Postman collection...");
await (mcpServer as SimplePostmanMcpServer).loadCollection(
collectionSource
);
// Load environment if specified
const environmentSource =
config.api.postman.environmentUrl || config.api.postman.environmentFile;
if (environmentSource) {
console.error("[SIMPLE-STDIO] Loading Postman environment...");
await (mcpServer as SimplePostmanMcpServer).loadEnvironment(
environmentSource
);
}
console.error("[SIMPLE-STDIO] Postman collection loaded successfully");
console.error(
"✅ Simple MCP Server successfully initialized with strategic tools!"
);
console.error("Now you have only 4 tools instead of hundreds:");
console.error(
" 1. list_requests - List all available requests in the collection"
);
console.error(
" 2. get_request_details - Get detailed info about specific requests"
);
console.error(" 3. search_requests - Search requests by keyword");
console.error(
" 4. make_request - Execute any request from the collection"
);
} else {
// Default to OpenAPI/Swagger with simplified tools
const openApiConfig = config.api.openapi || config.swagger;
if (!openApiConfig) {
throw new Error(
'OpenAPI configuration is required when api.type is "openapi" or for legacy swagger config'
);
}
console.error(
"[SIMPLE-STDIO] Creating simplified OpenAPI MCP server instance..."
);
mcpServer = new SimpleSwaggerMcpServer(
openApiConfig.apiBaseUrl,
openApiConfig.defaultAuth && openApiConfig.defaultAuth.type
? (openApiConfig.defaultAuth as any)
: undefined
);
console.error("[SIMPLE-STDIO] Loading OpenAPI specification...");
await mcpServer.loadSwaggerSpec(openApiConfig.url);
console.error("[SIMPLE-STDIO] OpenAPI specification loaded successfully");
console.error(
"✅ Simple MCP Server successfully initialized with strategic tools!"
);
console.error("Now you have only 4 tools instead of hundreds:");
console.error(" 1. list_endpoints - List all available API endpoints");
console.error(
" 2. get_endpoint_details - Get detailed info about specific endpoints"
);
console.error(" 3. search_endpoints - Search endpoints by keyword");
console.error(" 4. make_api_call - Make actual API calls");
}
// Get the MCP server instance
const server = mcpServer.getServer();
// Create stdio transport
console.error(`[SIMPLE-STDIO] Creating stdio transport...`);
const transport = new StdioServerTransport();
// Connect the MCP server to stdio transport
console.error(`[SIMPLE-STDIO] Connecting MCP server to stdio transport...`);
await server.connect(transport);
console.error(
"[SIMPLE-STDIO] Simplified MCP server connected via stdio successfully!"
);
console.error(
`[SIMPLE-STDIO] Server is ready and listening for requests...`
);
// Handle process termination gracefully
process.on("SIGINT", () => {
console.error(
"[SIMPLE-STDIO] Received SIGINT, shutting down gracefully..."
);
process.exit(0);
});
process.on("SIGTERM", () => {
console.error(
"[SIMPLE-STDIO] Received SIGTERM, shutting down gracefully..."
);
process.exit(0);
});
} catch (error) {
console.error(
"[SIMPLE-STDIO] Failed to start simplified MCP server:",
error
);
process.exit(1);
}
}
// Handle uncaught exceptions and rejections
process.on("uncaughtException", (error) => {
console.error("[SIMPLE-STDIO] Uncaught Exception:", error);
process.exit(1);
});
process.on("unhandledRejection", (reason, promise) => {
console.error(
"[SIMPLE-STDIO] Unhandled Rejection at:",
promise,
"reason:",
reason
);
process.exit(1);
});
main().catch((error) => {
console.error("[SIMPLE-STDIO] Unhandled error:", error);
process.exit(1);
});
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "libReplacement": true, /* Enable lib replacement. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "NodeNext", /* Specify what module code is generated. */
"moduleResolution": "NodeNext", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
"declarationMap": true, /* Create sourcemaps for d.ts files. */
"emitDeclarationOnly": false, /* Only output d.ts files and not JavaScript files. */
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
"noEmit": false, /* Disable emitting files from a compilation. */
"outDir": "dist", /* Specify an output folder for all emitted files. */
"removeComments": true, /* Disable emitting comments. */
"importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
"downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
"sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
"mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
"inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
"emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
"newLine": "crlf", /* Set the newline character for emitting files. */
"stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
"noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
"noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
"preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "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. */
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```
--------------------------------------------------------------------------------
/src/swagger-mcp-simple.ts:
--------------------------------------------------------------------------------
```typescript
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import axios from "axios";
import SwaggerParser from "@apidevtools/swagger-parser";
import { Request, Response } from "express";
import { AuthConfig } from "./types.js";
export class SimpleSwaggerMcpServer {
private mcpServer: McpServer;
private swaggerSpec: any = null;
private apiBaseUrl: string;
private defaultAuth: AuthConfig | undefined;
constructor(apiBaseUrl: string, defaultAuth?: AuthConfig) {
this.apiBaseUrl = apiBaseUrl;
this.defaultAuth = defaultAuth;
this.mcpServer = new McpServer({
name: "Simple Swagger API MCP Server",
version: "1.0.0",
});
}
async loadSwaggerSpec(specUrlOrFile: string) {
console.debug("Loading Swagger specification from:", specUrlOrFile);
try {
this.swaggerSpec = (await SwaggerParser.parse(specUrlOrFile)) as any;
const info = this.swaggerSpec.info;
console.debug("Loaded Swagger spec:", {
title: info.title,
version: info.version,
});
this.mcpServer = new McpServer({
name: info.title || "Swagger API Server",
version: info.version || "1.0.0",
description: info.description || undefined,
});
await this.registerTools();
} catch (error) {
console.error("Failed to load Swagger specification:", error);
throw error;
}
}
private getAuthHeaders(auth?: AuthConfig): Record<string, string> {
const authConfig = auth || this.defaultAuth;
if (!authConfig) return {};
switch (authConfig.type) {
case "basic":
if (authConfig.username && authConfig.password) {
const credentials = Buffer.from(
`${authConfig.username}:${authConfig.password}`
).toString("base64");
return { Authorization: `Basic ${credentials}` };
}
break;
case "bearer":
if (authConfig.token) {
return { Authorization: `Bearer ${authConfig.token}` };
}
break;
case "apiKey":
if (authConfig.apiKey && authConfig.apiKeyName) {
if (authConfig.apiKeyIn === "header") {
return { [authConfig.apiKeyName]: authConfig.apiKey };
}
}
break;
case "oauth2":
if (authConfig.token) {
return { Authorization: `Bearer ${authConfig.token}` };
}
break;
}
return {};
}
private getAuthQueryParams(auth?: AuthConfig): Record<string, string> {
const authConfig = auth || this.defaultAuth;
if (!authConfig) return {};
if (
authConfig.type === "apiKey" &&
authConfig.apiKey &&
authConfig.apiKeyName &&
authConfig.apiKeyIn === "query"
) {
return { [authConfig.apiKeyName]: authConfig.apiKey };
}
return {};
}
private async registerTools() {
console.debug("Starting tool registration process");
if (!this.swaggerSpec || !this.swaggerSpec.paths) {
console.warn("No paths found in Swagger spec");
return;
}
const paths = this.swaggerSpec.paths;
const totalPaths = Object.keys(paths).length;
console.debug(`Found ${totalPaths} paths to process`);
// Tool 1: List all available endpoints
this.mcpServer.tool(
"list_endpoints",
"List all available API endpoints with basic information including path, method, summary, and tags",
{
input: z.object({
method: z
.string()
.optional()
.describe("Filter by HTTP method (GET, POST, PUT, DELETE, etc.)"),
tag: z.string().optional().describe("Filter by OpenAPI tag"),
limit: z
.number()
.optional()
.default(50)
.describe("Maximum number of endpoints to return"),
}),
},
async ({ input }) => {
const endpoints = [];
for (const [path, pathItem] of Object.entries(paths)) {
if (!pathItem) continue;
for (const [method, operation] of Object.entries(pathItem as any)) {
if (method === "$ref" || !operation) continue;
const op = operation as any;
const operationId = op.operationId || `${method}-${path}`;
// Apply filters
if (
input.method &&
method.toLowerCase() !== input.method.toLowerCase()
)
continue;
if (input.tag && (!op.tags || !op.tags.includes(input.tag)))
continue;
endpoints.push({
operationId,
method: method.toUpperCase(),
path,
summary: op.summary || "",
description: op.description || "",
tags: op.tags || [],
deprecated: op.deprecated || false,
});
if (endpoints.length >= input.limit) break;
}
if (endpoints.length >= input.limit) break;
}
return {
content: [
{
type: "text",
text: JSON.stringify(
{
total: endpoints.length,
endpoints,
},
null,
2
),
},
],
};
}
);
// Tool 2: Get detailed information about a specific endpoint
this.mcpServer.tool(
"get_endpoint_details",
"Get detailed information about a specific API endpoint including parameters, request/response schemas, and authentication requirements",
{
input: z.object({
operationId: z.string().describe("The operation ID of the endpoint"),
path: z
.string()
.optional()
.describe("The API path (alternative to operationId)"),
method: z
.string()
.optional()
.describe("The HTTP method (required if using path)"),
}),
},
async ({ input }) => {
let targetOperation = null;
let targetPath = "";
let targetMethod = "";
// Find the operation by operationId or path+method
for (const [path, pathItem] of Object.entries(paths)) {
if (!pathItem) continue;
for (const [method, operation] of Object.entries(pathItem as any)) {
if (method === "$ref" || !operation) continue;
const op = operation as any;
const operationId = op.operationId || `${method}-${path}`;
if (
input.operationId === operationId ||
(input.path === path &&
input.method?.toLowerCase() === method.toLowerCase())
) {
targetOperation = op;
targetPath = path;
targetMethod = method;
break;
}
}
if (targetOperation) break;
}
if (!targetOperation) {
return {
content: [
{
type: "text",
text: `Endpoint not found. Use list_endpoints to see available endpoints.`,
},
],
};
}
// Extract parameter information
const parameters = (targetOperation.parameters || []).map(
(param: any) => ({
name: param.name,
in: param.in,
required: param.required || false,
type: param.schema?.type || param.type,
description: param.description || "",
example: param.example || param.schema?.example,
})
);
// Extract request body schema
let requestBody = null;
if (targetOperation.requestBody) {
const rb = targetOperation.requestBody;
const content = rb.content;
if (content) {
requestBody = Object.keys(content).map((mediaType) => ({
mediaType,
schema: content[mediaType].schema,
required: rb.required || false,
}));
}
}
// Extract response schemas
const responses = Object.entries(targetOperation.responses || {}).map(
([code, resp]: [string, any]) => ({
statusCode: code,
description: resp.description || "",
schema: resp.content
? Object.keys(resp.content).map((mt) => ({
mediaType: mt,
schema: resp.content[mt].schema,
}))
: null,
})
);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
operationId:
targetOperation.operationId ||
`${targetMethod}-${targetPath}`,
method: targetMethod.toUpperCase(),
path: targetPath,
summary: targetOperation.summary || "",
description: targetOperation.description || "",
tags: targetOperation.tags || [],
deprecated: targetOperation.deprecated || false,
parameters,
requestBody,
responses,
},
null,
2
),
},
],
};
}
);
// Tool 3: Search endpoints by keyword
this.mcpServer.tool(
"search_endpoints",
"Search API endpoints by keyword in path, summary, description, or tags",
{
input: z.object({
query: z
.string()
.describe(
"Search term to look for in endpoint paths, summaries, descriptions, or tags"
),
limit: z
.number()
.optional()
.default(20)
.describe("Maximum number of results to return"),
}),
},
async ({ input }) => {
const results = [];
const query = input.query.toLowerCase();
for (const [path, pathItem] of Object.entries(paths)) {
if (!pathItem) continue;
for (const [method, operation] of Object.entries(pathItem as any)) {
if (method === "$ref" || !operation) continue;
const op = operation as any;
const operationId = op.operationId || `${method}-${path}`;
// Search in various fields
const searchText = [
path,
op.summary || "",
op.description || "",
...(op.tags || []),
operationId,
]
.join(" ")
.toLowerCase();
if (searchText.includes(query)) {
results.push({
operationId,
method: method.toUpperCase(),
path,
summary: op.summary || "",
description: op.description || "",
tags: op.tags || [],
});
}
if (results.length >= input.limit) break;
}
if (results.length >= input.limit) break;
}
return {
content: [
{
type: "text",
text: JSON.stringify(
{
query: input.query,
total: results.length,
results,
},
null,
2
),
},
],
};
}
);
// Tool 4: Make API call
this.mcpServer.tool(
"make_api_call",
"Make an API call to any endpoint with the specified parameters and authentication",
{
input: z.object({
operationId: z
.string()
.optional()
.describe("The operation ID of the endpoint"),
path: z
.string()
.optional()
.describe("The API path (alternative to operationId)"),
method: z
.string()
.optional()
.describe("The HTTP method (required if using path)"),
parameters: z
.record(z.any())
.optional()
.describe("Query parameters, path parameters, or form data"),
body: z
.any()
.optional()
.describe("Request body (for POST, PUT, PATCH requests)"),
auth: z
.object({
type: z
.enum(["none", "basic", "bearer", "apiKey", "oauth2"])
.default("none"),
username: z.string().optional(),
password: z.string().optional(),
token: z.string().optional(),
apiKey: z.string().optional(),
apiKeyName: z.string().optional(),
apiKeyIn: z.enum(["header", "query"]).optional(),
})
.optional()
.describe("Authentication configuration"),
}),
},
async ({ input }) => {
// Find the operation
let targetOperation = null;
let targetPath = "";
let targetMethod = "";
for (const [path, pathItem] of Object.entries(paths)) {
if (!pathItem) continue;
for (const [method, operation] of Object.entries(pathItem as any)) {
if (method === "$ref" || !operation) continue;
const op = operation as any;
const operationId = op.operationId || `${method}-${path}`;
if (
input.operationId === operationId ||
(input.path === path &&
input.method?.toLowerCase() === method.toLowerCase())
) {
targetOperation = op;
targetPath = path;
targetMethod = method;
break;
}
}
if (targetOperation) break;
}
if (!targetOperation) {
return {
content: [
{
type: "text",
text: `Endpoint not found. Use list_endpoints to see available endpoints.`,
},
],
};
}
try {
const params = input.parameters || {};
let url = this.apiBaseUrl + targetPath;
// Handle path parameters
const pathParams = new Set();
targetPath.split("/").forEach((segment) => {
if (segment.startsWith("{") && segment.endsWith("}")) {
pathParams.add(segment.slice(1, -1));
}
});
Object.entries(params).forEach(([key, value]) => {
if (pathParams.has(key)) {
url = url.replace(`{${key}}`, encodeURIComponent(String(value)));
}
});
// Separate query parameters
const queryParams = Object.entries(params)
.filter(([key]) => !pathParams.has(key))
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
const headers = this.getAuthHeaders(
input.auth?.type !== "none" ? (input.auth as AuthConfig) : undefined
);
const authQueryParams = this.getAuthQueryParams(
input.auth?.type !== "none" ? (input.auth as AuthConfig) : undefined
);
const response = await axios({
method: targetMethod as string,
url: url,
headers,
data: input.body,
params: { ...queryParams, ...authQueryParams },
});
return {
content: [
{ type: "text", text: JSON.stringify(response.data, null, 2) },
{ type: "text", text: `HTTP Status Code: ${response.status}` },
],
};
} catch (error) {
console.error(`Error in API call:`, error);
if (axios.isAxiosError(error) && error.response) {
return {
content: [
{
type: "text",
text: `Error ${error.response.status}: ${JSON.stringify(
error.response.data,
null,
2
)}`,
},
],
};
}
return {
content: [{ type: "text", text: `Error: ${error}` }],
};
}
}
);
console.debug(
"Successfully registered 4 strategic tools for API navigation"
);
}
getServer() {
return this.mcpServer;
}
}
```
--------------------------------------------------------------------------------
/src/postman-mcp-simple.ts:
--------------------------------------------------------------------------------
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import axios from "axios";
import {
Collection,
Request as PostmanRequest,
Item,
ItemGroup,
} from "postman-collection";
import { Request, Response } from "express";
import { AuthConfig, ToolInput } from "./types.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
let transport: SSEServerTransport | null = null;
export class SimplePostmanMcpServer {
private mcpServer: McpServer;
private collection: Collection | null = null;
private environment: Record<string, any> = {};
private defaultAuth: AuthConfig | undefined;
private requests: Array<{
id: string;
name: string;
method: string;
url: string;
description: string;
folder: string;
request: PostmanRequest;
}> = [];
constructor(defaultAuth?: AuthConfig) {
if (process.env.NODE_ENV !== "production") {
console.debug("SimplePostmanMcpServer constructor", defaultAuth);
}
this.defaultAuth = defaultAuth;
this.mcpServer = new McpServer({
name: "Simple Postman Collection MCP Server",
version: "1.0.0",
});
}
private getAuthHeaders(auth?: AuthConfig): Record<string, string> {
const authConfig = auth || this.defaultAuth;
if (!authConfig) return {};
switch (authConfig.type) {
case "basic":
if (authConfig.username && authConfig.password) {
const credentials = Buffer.from(
`${authConfig.username}:${authConfig.password}`
).toString("base64");
return { Authorization: `Basic ${credentials}` };
}
break;
case "bearer":
if (authConfig.token) {
return { Authorization: `Bearer ${authConfig.token}` };
}
break;
case "apiKey":
if (
authConfig.apiKey &&
authConfig.apiKeyName &&
authConfig.apiKeyIn === "header"
) {
return { [authConfig.apiKeyName]: authConfig.apiKey };
}
break;
case "oauth2":
if (authConfig.token) {
return { Authorization: `Bearer ${authConfig.token}` };
}
break;
}
return {};
}
private getAuthQueryParams(auth?: AuthConfig): Record<string, string> {
const authConfig = auth || this.defaultAuth;
if (!authConfig) return {};
if (
authConfig.type === "apiKey" &&
authConfig.apiKey &&
authConfig.apiKeyName &&
authConfig.apiKeyIn === "query"
) {
return { [authConfig.apiKeyName]: authConfig.apiKey };
}
return {};
}
private createAuthSchema(): z.ZodType<any> {
return z
.object({
type: z
.enum(["none", "basic", "bearer", "apiKey", "oauth2"])
.default("none"),
username: z.string().optional(),
password: z.string().optional(),
token: z.string().optional(),
apiKey: z.string().optional(),
apiKeyName: z.string().optional(),
apiKeyIn: z.enum(["header", "query"]).optional(),
})
.describe("Authentication configuration for the request");
}
async loadCollection(collectionUrlOrFile: string) {
if (process.env.NODE_ENV !== "production") {
console.debug("Loading Postman collection from:", collectionUrlOrFile);
}
try {
let collectionData: any;
if (collectionUrlOrFile.startsWith("http")) {
const response = await axios.get(collectionUrlOrFile);
collectionData = response.data;
} else {
const fs = await import("fs/promises");
const fileContent = await fs.readFile(collectionUrlOrFile, "utf-8");
collectionData = JSON.parse(fileContent);
}
this.collection = new Collection(collectionData);
// Get collection info safely
const info = {
name:
(this.collection as any).name ||
collectionData.info?.name ||
"Postman Collection",
description:
(this.collection as any).description ||
collectionData.info?.description ||
"",
version:
(this.collection as any).version ||
collectionData.info?.version ||
"1.0.0",
};
if (process.env.NODE_ENV !== "production") {
console.debug("Loaded Postman collection:", {
name: info.name,
description:
typeof info.description === "string"
? info.description.substring(0, 100) + "..."
: "",
});
}
// Update server name with collection info
this.mcpServer = new McpServer({
name:
`${info.name} - Simple Explorer` ||
"Simple Postman Collection Server",
version: info.version || "1.0.0",
description: `Simplified explorer for ${info.name}` || undefined,
});
// Parse all requests for the strategic tools
this.parseAllRequests();
await this.registerStrategicTools();
} catch (error) {
console.error("Failed to load Postman collection:", error);
throw error;
}
}
async loadEnvironment(environmentUrlOrFile: string) {
if (process.env.NODE_ENV !== "production") {
console.debug("Loading Postman environment from:", environmentUrlOrFile);
}
try {
let environmentData: any;
if (environmentUrlOrFile.startsWith("http")) {
const response = await axios.get(environmentUrlOrFile);
environmentData = response.data;
} else {
const fs = await import("fs/promises");
const fileContent = await fs.readFile(environmentUrlOrFile, "utf-8");
environmentData = JSON.parse(fileContent);
}
// Parse environment variables
if (environmentData.values) {
for (const variable of environmentData.values) {
this.environment[variable.key] = variable.value;
}
}
if (process.env.NODE_ENV !== "production") {
console.debug(
"Loaded environment variables:",
Object.keys(this.environment)
);
}
} catch (error) {
console.error("Failed to load Postman environment:", error);
throw error;
}
}
private parseAllRequests() {
if (!this.collection) return;
this.requests = [];
const parseItem = (
item: Item | ItemGroup<Item>,
folderPath: string = ""
) => {
if (item instanceof Item && item.request) {
const request = item.request;
// Handle description safely
let description = "";
if (item.request.description) {
if (typeof item.request.description === "string") {
description = item.request.description;
} else if (
typeof item.request.description === "object" &&
"content" in item.request.description
) {
description = (item.request.description as any).content;
}
}
this.requests.push({
id: `${folderPath}${item.name}`
.replace(/[^a-zA-Z0-9_]/g, "_")
.toLowerCase(),
name: item.name || "Unnamed Request",
method: request.method || "GET",
url: request.url?.toString() || "",
description,
folder: folderPath,
request,
});
} else if (item instanceof ItemGroup) {
// Handle folders recursively
const newFolderPath = folderPath
? `${folderPath}/${item.name}`
: item.name;
item.items.each((subItem: Item | ItemGroup<Item>) => {
parseItem(subItem, newFolderPath);
});
}
};
// Parse all items
this.collection.items.each((item: Item | ItemGroup<Item>) => {
parseItem(item);
});
console.log(
`✅ Parsed ${this.requests.length} requests from Postman collection`
);
}
private resolveVariables(text: string): string {
if (!text) return text;
// Replace {{variableName}} with actual values
return text.replace(/\{\{(\w+)\}\}/g, (match, variableName) => {
return this.environment[variableName] || match;
});
}
private async registerStrategicTools() {
// Tool 1: List all requests
this.mcpServer.tool(
"list_requests",
"List all available requests in the Postman collection with basic information",
{
input: z.object({
method: z
.string()
.optional()
.describe("Filter by HTTP method (GET, POST, PUT, DELETE, etc.)"),
folder: z.string().optional().describe("Filter by folder/path"),
limit: z
.number()
.optional()
.default(50)
.describe("Maximum number of requests to return"),
}),
},
async ({ input }) => {
let filteredRequests = this.requests;
// Apply filters
if (input.method) {
filteredRequests = filteredRequests.filter(
(req) => req.method.toLowerCase() === input.method!.toLowerCase()
);
}
if (input.folder) {
filteredRequests = filteredRequests.filter((req) =>
req.folder.toLowerCase().includes(input.folder!.toLowerCase())
);
}
// Limit results
const limitedRequests = filteredRequests.slice(0, input.limit);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
total: limitedRequests.length,
requests: limitedRequests.map((req) => ({
id: req.id,
name: req.name,
method: req.method,
url: req.url,
folder: req.folder,
description:
req.description.substring(0, 100) +
(req.description.length > 100 ? "..." : ""),
})),
},
null,
2
),
},
],
};
}
);
// Tool 2: Get detailed information about a specific request
this.mcpServer.tool(
"get_request_details",
"Get detailed information about a specific request including parameters, headers, and body structure",
{
input: z.object({
requestId: z.string().describe("The ID of the request"),
name: z
.string()
.optional()
.describe("The name of the request (alternative to ID)"),
}),
},
async ({ input }) => {
let targetRequest = null;
// Find the request by ID or name
for (const req of this.requests) {
if (
req.id === input.requestId ||
req.name.toLowerCase() === input.name?.toLowerCase()
) {
targetRequest = req;
break;
}
}
if (!targetRequest) {
return {
content: [
{
type: "text",
text: `Request not found. Use list_requests to see available requests.`,
},
],
};
}
const request = targetRequest.request;
// Extract parameters information
const parameters = {
query: [] as any[],
path: [] as any[],
headers: [] as any[],
};
// Query parameters
if (request.url && request.url.query) {
request.url.query.each((param: any) => {
if (param.key && !param.disabled) {
parameters.query.push({
name: param.key,
value: param.value || "",
description: param.description || "",
});
}
});
}
// Path variables
if (request.url && request.url.variables) {
request.url.variables.each((variable: any) => {
if (variable.key) {
parameters.path.push({
name: variable.key,
value: variable.value || "",
description: variable.description || "",
});
}
});
}
// Headers
if (request.headers) {
request.headers.each((header: any) => {
if (header.key && !header.disabled) {
parameters.headers.push({
name: header.key,
value: header.value || "",
description: header.description || "",
});
}
});
}
// Request body info
let bodyInfo: any = null;
if (
request.body &&
["POST", "PUT", "PATCH"].includes(request.method || "")
) {
bodyInfo = {
mode: request.body.mode,
description: "Request body based on the collection definition",
} as any;
if (request.body.mode === "raw") {
bodyInfo.example = request.body.raw || "";
} else if (request.body.mode === "formdata") {
bodyInfo.formFields = [];
if (request.body.formdata) {
request.body.formdata.each((field: any) => {
if (field.key && !field.disabled) {
bodyInfo.formFields.push({
name: field.key,
type: field.type || "text",
value: field.value || "",
description: field.description || "",
});
}
});
}
}
}
return {
content: [
{
type: "text",
text: JSON.stringify(
{
id: targetRequest.id,
name: targetRequest.name,
method: targetRequest.method,
url: targetRequest.url,
folder: targetRequest.folder,
description: targetRequest.description,
parameters,
body: bodyInfo,
auth: "Use the auth parameter in make_request to provide authentication",
},
null,
2
),
},
],
};
}
);
// Tool 3: Search requests by keyword
this.mcpServer.tool(
"search_requests",
"Search requests by keyword in name, description, URL, or folder",
{
input: z.object({
query: z
.string()
.describe(
"Search term to look for in request names, descriptions, URLs, or folders"
),
limit: z
.number()
.optional()
.default(20)
.describe("Maximum number of results to return"),
}),
},
async ({ input }) => {
const query = input.query.toLowerCase();
const results = [];
for (const req of this.requests) {
const searchText = [
req.name,
req.description,
req.url,
req.folder,
req.method,
]
.join(" ")
.toLowerCase();
if (searchText.includes(query)) {
results.push({
id: req.id,
name: req.name,
method: req.method,
url: req.url,
folder: req.folder,
description:
req.description.substring(0, 100) +
(req.description.length > 100 ? "..." : ""),
relevance: this.calculateRelevance(query, searchText),
});
if (results.length >= input.limit) break;
}
}
// Sort by relevance
results.sort((a, b) => b.relevance - a.relevance);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
query: input.query,
total: results.length,
results: results.map((r) => ({ ...r, relevance: undefined })), // Remove relevance from output
},
null,
2
),
},
],
};
}
);
// Tool 4: Make request
this.mcpServer.tool(
"make_request",
"Execute any request from the Postman collection with the specified parameters and authentication",
{
input: z.object({
requestId: z.string().optional().describe("The ID of the request"),
name: z
.string()
.optional()
.describe("The name of the request (alternative to ID)"),
parameters: z
.record(z.any())
.optional()
.describe(
"Query parameters, path parameters, headers, or form data"
),
body: z
.any()
.optional()
.describe("Request body (for POST, PUT, PATCH requests)"),
auth: this.createAuthSchema()
.optional()
.describe("Authentication configuration"),
}),
},
async ({ input }) => {
// Find the request
let targetRequest = null;
for (const req of this.requests) {
if (
req.id === input.requestId ||
req.name.toLowerCase() === input.name?.toLowerCase()
) {
targetRequest = req;
break;
}
}
if (!targetRequest) {
return {
content: [
{
type: "text",
text: `Request not found. Use list_requests to see available requests.`,
},
],
};
}
try {
const result = await this.executeRequest(
targetRequest.request,
input.parameters || {},
input.body,
input.auth
);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
};
}
}
);
console.log(
"✅ Successfully registered 4 strategic tools for Postman collection exploration"
);
}
private calculateRelevance(query: string, text: string): number {
const queryWords = query.split(" ");
let score = 0;
queryWords.forEach((word) => {
if (text.includes(word)) {
score += 1;
// Bonus for exact word matches
if (
text.includes(` ${word} `) ||
text.startsWith(word) ||
text.endsWith(word)
) {
score += 0.5;
}
}
});
return score;
}
private async executeRequest(
request: PostmanRequest,
parameters: Record<string, any>,
body?: any,
auth?: AuthConfig
): Promise<any> {
// Build URL
let url = request.url?.toString() || "";
url = this.resolveVariables(url);
// Replace path variables
Object.keys(parameters).forEach((key) => {
const value = parameters[key];
if (value !== undefined) {
// Try different variable formats
url = url.replace(`:${key}`, encodeURIComponent(String(value)));
url = url.replace(`{{${key}}}`, encodeURIComponent(String(value)));
url = url.replace(`{${key}}`, encodeURIComponent(String(value)));
}
});
// Build query parameters
const queryParams = this.getAuthQueryParams(auth);
Object.keys(parameters).forEach((key) => {
const value = parameters[key];
if (
value !== undefined &&
!url.includes(`:${key}`) &&
!url.includes(`{{${key}}}`)
) {
queryParams[key] = value;
}
});
// Build headers
const headers = this.getAuthHeaders(auth);
// Add any headers from parameters
Object.keys(parameters).forEach((key) => {
const value = parameters[key];
if (value !== undefined && key.toLowerCase().includes("header")) {
headers[key] = value;
}
});
// Add default content type for requests with body
if (body && !headers["Content-Type"]) {
headers["Content-Type"] = "application/json";
}
// Prepare request configuration
const config: any = {
method: request.method || "GET",
url,
headers,
params: queryParams,
};
// Add body if present
if (body) {
if (typeof body === "string") {
config.data = body;
} else {
config.data = JSON.stringify(body);
}
}
if (process.env.NODE_ENV !== "production") {
console.debug("Executing request:", {
method: config.method,
url: config.url,
headers: Object.keys(config.headers),
hasBody: !!config.data,
});
}
try {
const response = await axios(config);
return {
status: response.status,
statusText: response.statusText,
headers: response.headers,
data: response.data,
};
} catch (error: any) {
if (error.response) {
return {
status: error.response.status,
statusText: error.response.statusText,
headers: error.response.headers,
data: error.response.data,
error: true,
};
}
throw error;
}
}
getServer() {
return this.mcpServer;
}
handleSSE(res: Response) {
if (!transport) {
transport = new SSEServerTransport("/messages", res);
}
this.mcpServer.connect(transport);
}
handleMessage(req: Request, res: Response) {
this.mcpServer.connect(transport!);
}
}
```