Swagger Explorer MCP
by johnneerdael
- swagger-mcp
- dist
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SwaggerExplorerMCP = void 0;
const express_1 = __importDefault(require("express"));
const playwright_1 = require("playwright");
const yaml_1 = __importDefault(require("yaml"));
const net_1 = require("net");
async function findAvailablePort(startPort = 3000, endPort = 65535) {
for (let port = startPort; port <= endPort; port++) {
try {
const server = (0, net_1.createServer)();
const available = await new Promise((resolve, reject) => {
server.once('error', (err) => {
if (err.code === 'EADDRINUSE') {
resolve(false);
}
else {
reject(err);
}
});
server.once('listening', () => {
server.close();
resolve(true);
});
server.listen(port);
});
if (available) {
return port;
}
}
catch (err) {
continue;
}
}
throw new Error('No available ports found');
}
class SwaggerExplorerMCP {
constructor(config = {}) {
this.config = {
...config
};
this.app = (0, express_1.default)();
this.schemaCache = new Map();
this.setupMiddleware();
this.setupRoutes();
}
setupMiddleware() {
this.app.use(express_1.default.json());
// Authentication middleware
const authMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!this.config.authToken) {
return next();
}
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or invalid authorization header' });
}
const token = authHeader.split(' ')[1];
if (token !== this.config.authToken) {
return res.status(401).json({ error: 'Invalid token' });
}
next();
};
// Apply auth middleware to all routes except health
this.app.use((req, res, next) => {
if (req.path === '/health') {
return next();
}
authMiddleware(req, res, next);
});
}
setupRoutes() {
const basePath = this.config.baseUrl || '';
// Custom response format handler
const formatResponse = (data, format) => {
switch (format === null || format === void 0 ? void 0 : format.toLowerCase()) {
case 'minimal':
return {
status: 'success',
data: this.minimizeResponse(data)
};
case 'detailed':
return {
status: 'success',
timestamp: new Date().toISOString(),
data,
metadata: {
version: '1.0',
format: 'detailed'
}
};
default:
return data;
}
};
// Health check endpoint
this.app.get('/health', (req, res) => {
res.json({
status: 'healthy',
port: this.port,
baseUrl: this.config.baseUrl
});
});
// Main API to explore Swagger
this.app.post(`${basePath}/api/explore`, async (req, res) => {
try {
const { url, options = {} } = req.body;
if (!url) {
return res.status(400).json({ error: 'URL is required' });
}
const page = await this.browser.newPage();
try {
const swaggerData = await this.getSwaggerData(url, page);
const result = await this.processSwaggerData(swaggerData, options);
res.json(formatResponse(result, req.body.format));
}
finally {
await page.close();
}
}
catch (error) {
console.error('Error exploring Swagger:', error);
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
}
});
// Get response schemas for a path
this.app.post(`${basePath}/api/response-schemas`, async (req, res) => {
try {
const { url, path, method, format } = req.body;
if (!url || !path || !method) {
return res.status(400).json({ error: 'URL, path, and method are required' });
}
const page = await this.browser.newPage();
try {
const swaggerData = await this.getSwaggerData(url, page);
const responses = await this.extractResponseSchemas(swaggerData, path, method);
res.json(formatResponse(responses, format));
}
finally {
await page.close();
}
}
catch (error) {
console.error('Error getting response schemas:', error);
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
}
});
}
minimizeResponse(data) {
if (Array.isArray(data)) {
return data.map(item => this.minimizeResponse(item));
}
if (typeof data === 'object' && data !== null) {
const minimized = {};
for (const [key, value] of Object.entries(data)) {
if (value !== null && value !== undefined && value !== '') {
minimized[key] = this.minimizeResponse(value);
}
}
return minimized;
}
return data;
}
async processSwaggerData(swaggerData, options) {
var _a;
const result = {};
if (options.paths) {
result.paths = Object.entries(swaggerData.paths || {})
.filter(([_, methods]) => {
var _a;
if (!((_a = options.methodFilter) === null || _a === void 0 ? void 0 : _a.length))
return true;
const methodKeys = Object.keys(methods);
return methodKeys.some(method => { var _a, _b; return (_b = (_a = options.methodFilter) === null || _a === void 0 ? void 0 : _a.includes(method.toLowerCase())) !== null && _b !== void 0 ? _b : false; });
})
.map(([path, methods]) => ({
path,
methods: Object.keys(methods)
}));
}
if (options.schemas) {
const schemas = ((_a = swaggerData.components) === null || _a === void 0 ? void 0 : _a.schemas) || swaggerData.definitions || {};
result.schemas = Object.keys(schemas);
}
return result;
}
async extractResponseSchemas(swaggerData, path, method) {
var _a, _b;
const responses = ((_b = (_a = swaggerData.paths[path]) === null || _a === void 0 ? void 0 : _a[method]) === null || _b === void 0 ? void 0 : _b.responses) || {};
return Object.entries(responses).map(([code, response]) => ({
code,
description: response.description || '',
formats: Object.entries(response.content || {}).map(([contentType, content]) => ({
contentType,
schema: content.schema,
example: content.example,
encoding: content.encoding
}))
}));
}
async start() {
try {
this.browser = await playwright_1.chromium.launch();
// Find an available port if none is specified
if (!this.port) {
this.port = await findAvailablePort();
}
return new Promise((resolve, reject) => {
const server = this.app.listen(this.port)
.once('listening', () => {
console.log(`Swagger Explorer MCP running on port ${this.port}`);
if (this.config.baseUrl) {
console.log(`Base URL: ${this.config.baseUrl}`);
}
if (this.config.authToken) {
console.log('Authentication enabled');
}
resolve(this.port);
})
.once('error', async (err) => {
if (err.code === 'EADDRINUSE') {
console.log(`Port ${this.port} in use, trying another port...`);
this.port = undefined; // Reset port to try again
try {
const newPort = await this.start();
resolve(newPort);
}
catch (retryError) {
reject(retryError);
}
}
else {
console.error('Failed to start MCP:', err);
reject(err);
}
});
// Store server reference for proper shutdown
this.server = server;
});
}
catch (error) {
console.error('Failed to start MCP:', error);
throw error;
}
}
async stop() {
if (this.browser) {
await this.browser.close();
}
if (this.server) {
await new Promise((resolve) => {
this.server.close(() => resolve());
});
}
process.exit(0);
}
async getSwaggerData(url, page) {
let swaggerData = null;
// Listen for swagger spec in network requests
const responsePromise = new Promise((resolve) => {
page.on('response', async (response) => {
const responseUrl = response.url();
if (responseUrl.includes('swagger') || responseUrl.includes('openapi')) {
try {
const data = await response.json();
resolve(data);
}
catch (e) {
const text = await response.text();
if (text.includes('openapi:') || text.includes('swagger:')) {
resolve(yaml_1.default.parse(text));
}
}
}
});
});
await page.goto(url, { waitUntil: 'networkidle' });
// Try network response first
swaggerData = await Promise.race([
responsePromise,
new Promise(resolve => setTimeout(resolve, 5000))
]);
// Fallback to window object if needed
if (!swaggerData) {
swaggerData = await page.evaluate(() => {
var _a, _b;
// @ts-ignore
return (_b = (_a = window.ui) === null || _a === void 0 ? void 0 : _a.spec) === null || _b === void 0 ? void 0 : _b.json;
});
}
if (!swaggerData) {
throw new Error('Could not find Swagger/OpenAPI specification');
}
return swaggerData;
}
}
exports.SwaggerExplorerMCP = SwaggerExplorerMCP;
// Handle graceful shutdown
process.on('SIGTERM', async () => {
console.log('Received SIGTERM. Shutting down gracefully...');
if (global.mcp) {
await global.mcp.stop();
}
});
process.on('SIGINT', async () => {
console.log('Received SIGINT. Shutting down gracefully...');
if (global.mcp) {
await global.mcp.stop();
}
});