swagger-mcp-simple.ts•16.5 kB
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;
  }
}