#
tokens: 29344/50000 12/75 files (page 2/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 2 of 2. Use http://codebase.md/kadykov/mcp-openapi-schema-explorer?lines=false&page={x} to view the full context.

# Directory Structure

```
├── .devcontainer
│   ├── devcontainer.json
│   └── Dockerfile
├── .github
│   ├── dependabot.yml
│   └── workflows
│       └── ci.yml
├── .gitignore
├── .husky
│   └── pre-commit
├── .prettierignore
├── .prettierrc.json
├── .releaserc.json
├── .tokeignore
├── assets
│   ├── logo-400.png
│   └── logo-full.png
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Dockerfile
├── DOCKERHUB_README.md
├── eslint.config.js
├── jest.config.js
├── justfile
├── LICENSE
├── llms-install.md
├── memory-bank
│   ├── activeContext.md
│   ├── productContext.md
│   ├── progress.md
│   ├── projectbrief.md
│   ├── systemPatterns.md
│   └── techContext.md
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   └── generate-version.js
├── src
│   ├── config.ts
│   ├── handlers
│   │   ├── component-detail-handler.ts
│   │   ├── component-map-handler.ts
│   │   ├── handler-utils.ts
│   │   ├── operation-handler.ts
│   │   ├── path-item-handler.ts
│   │   └── top-level-field-handler.ts
│   ├── index.ts
│   ├── rendering
│   │   ├── components.ts
│   │   ├── document.ts
│   │   ├── path-item.ts
│   │   ├── paths.ts
│   │   ├── types.ts
│   │   └── utils.ts
│   ├── services
│   │   ├── formatters.ts
│   │   ├── reference-transform.ts
│   │   └── spec-loader.ts
│   ├── types.ts
│   ├── utils
│   │   └── uri-builder.ts
│   └── version.ts
├── test
│   ├── __tests__
│   │   ├── e2e
│   │   │   ├── format.test.ts
│   │   │   ├── resources.test.ts
│   │   │   └── spec-loading.test.ts
│   │   └── unit
│   │       ├── config.test.ts
│   │       ├── handlers
│   │       │   ├── component-detail-handler.test.ts
│   │       │   ├── component-map-handler.test.ts
│   │       │   ├── handler-utils.test.ts
│   │       │   ├── operation-handler.test.ts
│   │       │   ├── path-item-handler.test.ts
│   │       │   └── top-level-field-handler.test.ts
│   │       ├── rendering
│   │       │   ├── components.test.ts
│   │       │   ├── document.test.ts
│   │       │   ├── path-item.test.ts
│   │       │   └── paths.test.ts
│   │       ├── services
│   │       │   ├── formatters.test.ts
│   │       │   ├── reference-transform.test.ts
│   │       │   └── spec-loader.test.ts
│   │       └── utils
│   │           └── uri-builder.test.ts
│   ├── fixtures
│   │   ├── complex-endpoint.json
│   │   ├── empty-api.json
│   │   ├── multi-component-types.json
│   │   ├── paths-test.json
│   │   ├── sample-api.json
│   │   └── sample-v2-api.json
│   ├── setup.ts
│   └── utils
│       ├── console-helpers.ts
│       ├── mcp-test-helpers.ts
│       └── test-types.ts
├── tsconfig.json
└── tsconfig.test.json
```

# Files

--------------------------------------------------------------------------------
/src/rendering/components.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPIV3 } from 'openapi-types';
import { RenderableSpecObject, RenderContext, RenderResultItem } from './types.js'; // Add .js
import { createErrorResult, generateListHint } from './utils.js'; // Add .js

// Define valid component types based on OpenAPIV3.ComponentsObject keys
export type ComponentType = keyof OpenAPIV3.ComponentsObject;
export const VALID_COMPONENT_TYPES: ComponentType[] = [
  'schemas',
  'responses',
  'parameters',
  'examples',
  'requestBodies',
  'headers',
  'securitySchemes',
  'links',
  'callbacks',
  // 'pathItems' is technically allowed but we handle paths separately
];

// Simple descriptions for component types
const componentTypeDescriptions: Record<ComponentType, string> = {
  schemas: 'Reusable data structures (models)',
  responses: 'Reusable API responses',
  parameters: 'Reusable request parameters (query, path, header, cookie)',
  examples: 'Reusable examples of media type payloads',
  requestBodies: 'Reusable request body definitions',
  headers: 'Reusable header definitions for responses',
  securitySchemes: 'Reusable security scheme definitions (e.g., API keys, OAuth2)',
  links: 'Reusable descriptions of links between responses and operations',
  callbacks: 'Reusable descriptions of callback operations',
  // pathItems: 'Reusable path item definitions (rarely used directly here)' // Excluded as per comment above
};
// Use a Map for safer lookups against prototype pollution
const componentDescriptionsMap = new Map(Object.entries(componentTypeDescriptions));

/**
 * Wraps an OpenAPIV3.ComponentsObject to make it renderable.
 * Handles listing the available component types.
 */
export class RenderableComponents implements RenderableSpecObject {
  constructor(private components: OpenAPIV3.ComponentsObject | undefined) {}

  /**
   * Renders a list of available component types found in the spec.
   * Corresponds to the `openapi://components` URI.
   */
  renderList(context: RenderContext): RenderResultItem[] {
    if (!this.components || Object.keys(this.components).length === 0) {
      return createErrorResult('components', 'No components found in the specification.');
    }

    const availableTypes = Object.keys(this.components).filter((key): key is ComponentType =>
      VALID_COMPONENT_TYPES.includes(key as ComponentType)
    );

    if (availableTypes.length === 0) {
      return createErrorResult('components', 'No valid component types found.');
    }

    let listText = 'Available Component Types:\n\n';
    availableTypes.sort().forEach(type => {
      const description = componentDescriptionsMap.get(type) ?? 'Unknown component type'; // Removed unnecessary 'as ComponentType'
      listText += `- ${type}: ${description}\n`;
    });

    // Use the new hint generator structure, providing the first type as an example
    const firstTypeExample = availableTypes.length > 0 ? availableTypes[0] : undefined;
    listText += generateListHint(context, {
      itemType: 'componentType',
      firstItemExample: firstTypeExample,
    });

    return [
      {
        uriSuffix: 'components',
        data: listText,
        renderAsList: true,
      },
    ];
  }

  /**
   * Detail view for the main 'components' object isn't meaningful.
   */
  renderDetail(context: RenderContext): RenderResultItem[] {
    return this.renderList(context);
  }

  /**
   * Gets the map object for a specific component type.
   * @param type - The component type (e.g., 'schemas').
   * @returns The map (e.g., ComponentsObject['schemas']) or undefined.
   */
  getComponentMap(type: ComponentType):
    | Record<
        string,
        | OpenAPIV3.SchemaObject
        | OpenAPIV3.ResponseObject
        | OpenAPIV3.ParameterObject
        | OpenAPIV3.ExampleObject
        | OpenAPIV3.RequestBodyObject
        | OpenAPIV3.HeaderObject
        | OpenAPIV3.SecuritySchemeObject
        | OpenAPIV3.LinkObject
        | OpenAPIV3.CallbackObject
        | OpenAPIV3.ReferenceObject // Include ReferenceObject
      >
    | undefined {
    // Use Map for safe access
    if (!this.components) {
      return undefined;
    }
    const componentsMap = new Map(Object.entries(this.components));
    // Cast needed as Map.get returns the value type or undefined
    return componentsMap.get(type) as ReturnType<RenderableComponents['getComponentMap']>;
  }
}

// =====================================================================

/**
 * Wraps a map of components of a specific type (e.g., all schemas).
 * Handles listing component names and rendering component details.
 */
export class RenderableComponentMap implements RenderableSpecObject {
  constructor(
    private componentMap: ReturnType<RenderableComponents['getComponentMap']>,
    private componentType: ComponentType, // e.g., 'schemas'
    private mapUriSuffix: string // e.g., 'components/schemas'
  ) {}

  /**
   * Renders a list of component names for the specific type.
   * Corresponds to the `openapi://components/{type}` URI.
   */
  renderList(context: RenderContext): RenderResultItem[] {
    if (!this.componentMap || Object.keys(this.componentMap).length === 0) {
      return createErrorResult(
        this.mapUriSuffix,
        `No components of type "${this.componentType}" found.`
      );
    }

    const names = Object.keys(this.componentMap).sort();
    let listText = `Available ${this.componentType}:\n\n`;
    names.forEach(name => {
      listText += `- ${name}\n`;
    });

    // Use the new hint generator structure, providing parent type and first name as example
    const firstNameExample = names.length > 0 ? names[0] : undefined;
    listText += generateListHint(context, {
      itemType: 'componentName',
      parentComponentType: this.componentType,
      firstItemExample: firstNameExample,
    });

    return [
      {
        uriSuffix: this.mapUriSuffix,
        data: listText,
        renderAsList: true,
      },
    ];
  }

  /**
   * Renders the detail view for one or more specific named components
   * Renders the detail view. For a component map, this usually means listing
   * the component names, similar to renderList. The handler should call
   * `renderComponentDetail` for specific component details.
   */
  renderDetail(context: RenderContext): RenderResultItem[] {
    // Delegate to renderList as the primary view for a component map itself.
    return this.renderList(context);
  }

  /**
   * Renders the detail view for one or more specific named components
   * within this map.
   * Corresponds to the `openapi://components/{type}/{name*}` URI.
   * This is called by the handler after identifying the name(s).
   *
   * @param _context - The rendering context (might be needed later).
   * @param names - Array of component names.
   * @returns An array of RenderResultItem representing the component details.
   */
  renderComponentDetail(_context: RenderContext, names: string[]): RenderResultItem[] {
    if (!this.componentMap) {
      // Create error results for all requested names if map is missing
      return names.map(name => ({
        uriSuffix: `${this.mapUriSuffix}/${name}`,
        data: null,
        isError: true,
        errorText: `Component map for type "${this.componentType}" not found.`,
        renderAsList: true,
      }));
    }

    const results: RenderResultItem[] = [];
    for (const name of names) {
      const component = this.getComponent(name);
      const componentUriSuffix = `${this.mapUriSuffix}/${name}`;

      if (!component) {
        results.push({
          uriSuffix: componentUriSuffix,
          data: null,
          isError: true,
          errorText: `Component "${name}" of type "${this.componentType}" not found.`,
          renderAsList: true,
        });
      } else {
        // Return the raw component object; handler will format it
        results.push({
          uriSuffix: componentUriSuffix,
          data: component,
        });
      }
    }
    return results;
  }

  /**
   * Gets a specific component object by name.
   * @param name - The name of the component.
   * @returns The component object (or ReferenceObject) or undefined.
   */
  getComponent(
    name: string
  ):
    | OpenAPIV3.SchemaObject
    | OpenAPIV3.ResponseObject
    | OpenAPIV3.ParameterObject
    | OpenAPIV3.ExampleObject
    | OpenAPIV3.RequestBodyObject
    | OpenAPIV3.HeaderObject
    | OpenAPIV3.SecuritySchemeObject
    | OpenAPIV3.LinkObject
    | OpenAPIV3.CallbackObject
    | OpenAPIV3.ReferenceObject
    | undefined {
    // Use Map for safe access
    if (!this.componentMap) {
      return undefined;
    }
    const detailsMap = new Map(Object.entries(this.componentMap));
    // No cast needed, Map.get returns the correct type (ValueType | undefined)
    return detailsMap.get(name);
  }
}

```

--------------------------------------------------------------------------------
/test/__tests__/unit/services/spec-loader.test.ts:
--------------------------------------------------------------------------------

```typescript
import { SpecLoaderService } from '../../../../src/services/spec-loader.js';
import { ReferenceTransformService } from '../../../../src/services/reference-transform.js';
import { OpenAPIV3 } from 'openapi-types';

// Define mock implementations first
const mockConvertUrlImplementation = jest.fn();
const mockConvertFileImplementation = jest.fn();

// Mock the module, referencing the defined implementations
// IMPORTANT: The factory function for jest.mock runs BEFORE top-level variable assignments in the module scope.
// We need to access the mocks indirectly.
interface Swagger2OpenapiResult {
  openapi: OpenAPIV3.Document;
  options: unknown; // Use unknown for options as we don't have precise types here
}

jest.mock('swagger2openapi', () => {
  // Return an object where the properties are functions that call our mocks
  return {
    convertUrl: (url: string, options: unknown): Promise<Swagger2OpenapiResult> =>
      mockConvertUrlImplementation(url, options) as Promise<Swagger2OpenapiResult>, // Cast return type
    convertFile: (filename: string, options: unknown): Promise<Swagger2OpenapiResult> =>
      mockConvertFileImplementation(filename, options) as Promise<Swagger2OpenapiResult>, // Cast return type
  };
});

describe('SpecLoaderService', () => {
  const mockV3Spec: OpenAPIV3.Document = {
    openapi: '3.0.0',
    info: {
      title: 'Test V3 API',
      version: '1.0.0',
    },
    paths: {},
  };

  // Simulate the structure returned by swagger2openapi
  const mockS2OResult = {
    openapi: mockV3Spec,
    options: {}, // Add other properties if needed by tests
  };

  let referenceTransform: ReferenceTransformService;

  beforeEach(() => {
    // Reset the mock implementations
    mockConvertUrlImplementation.mockReset();
    mockConvertFileImplementation.mockReset();
    referenceTransform = new ReferenceTransformService();
    // Mock the transformDocument method for simplicity in these tests
    jest.spyOn(referenceTransform, 'transformDocument').mockImplementation(spec => spec);
  });

  describe('loadSpec', () => {
    it('loads local v3 spec using convertFile', async () => {
      mockConvertFileImplementation.mockResolvedValue(mockS2OResult);
      const loader = new SpecLoaderService('/path/to/spec.json', referenceTransform);
      const spec = await loader.loadSpec();

      expect(mockConvertFileImplementation).toHaveBeenCalledWith(
        '/path/to/spec.json',
        expect.any(Object)
      );
      expect(mockConvertUrlImplementation).not.toHaveBeenCalled();
      expect(spec).toEqual(mockV3Spec);
    });

    it('loads remote v3 spec using convertUrl', async () => {
      mockConvertUrlImplementation.mockResolvedValue(mockS2OResult);
      const loader = new SpecLoaderService('http://example.com/spec.json', referenceTransform);
      const spec = await loader.loadSpec();

      expect(mockConvertUrlImplementation).toHaveBeenCalledWith(
        'http://example.com/spec.json',
        expect.any(Object)
      );
      expect(mockConvertFileImplementation).not.toHaveBeenCalled();
      expect(spec).toEqual(mockV3Spec);
    });

    it('loads and converts local v2 spec using convertFile', async () => {
      // Assume convertFile handles v2 internally and returns v3
      mockConvertFileImplementation.mockResolvedValue(mockS2OResult);
      const loader = new SpecLoaderService('/path/to/v2spec.json', referenceTransform);
      const spec = await loader.loadSpec();

      expect(mockConvertFileImplementation).toHaveBeenCalledWith(
        '/path/to/v2spec.json',
        expect.any(Object)
      );
      expect(mockConvertUrlImplementation).not.toHaveBeenCalled();
      expect(spec).toEqual(mockV3Spec); // Should be the converted v3 spec
    });

    it('loads and converts remote v2 spec using convertUrl', async () => {
      // Assume convertUrl handles v2 internally and returns v3
      mockConvertUrlImplementation.mockResolvedValue(mockS2OResult);
      const loader = new SpecLoaderService('https://example.com/v2spec.yaml', referenceTransform);
      const spec = await loader.loadSpec();

      expect(mockConvertUrlImplementation).toHaveBeenCalledWith(
        'https://example.com/v2spec.yaml',
        expect.any(Object)
      );
      expect(mockConvertFileImplementation).not.toHaveBeenCalled();
      expect(spec).toEqual(mockV3Spec); // Should be the converted v3 spec
    });

    it('throws error if convertFile fails', async () => {
      const loadError = new Error('File not found');
      mockConvertFileImplementation.mockRejectedValue(loadError);
      const loader = new SpecLoaderService('/path/to/spec.json', referenceTransform);

      await expect(loader.loadSpec()).rejects.toThrow(
        'Failed to load/convert OpenAPI spec from /path/to/spec.json: File not found'
      );
    });

    it('throws error if convertUrl fails', async () => {
      const loadError = new Error('Network error');
      mockConvertUrlImplementation.mockRejectedValue(loadError);
      const loader = new SpecLoaderService('http://example.com/spec.json', referenceTransform);

      await expect(loader.loadSpec()).rejects.toThrow(
        'Failed to load/convert OpenAPI spec from http://example.com/spec.json: Network error'
      );
    });

    it('throws error if result object is invalid', async () => {
      mockConvertFileImplementation.mockResolvedValue({ options: {} }); // Missing openapi property
      const loader = new SpecLoaderService('/path/to/spec.json', referenceTransform);

      await expect(loader.loadSpec()).rejects.toThrow(
        'Failed to load/convert OpenAPI spec from /path/to/spec.json: Conversion or parsing failed to produce an OpenAPI document.'
      );
    });
  });

  describe('getSpec', () => {
    it('returns loaded spec after loadSpec called', async () => {
      mockConvertFileImplementation.mockResolvedValue(mockS2OResult);
      const loader = new SpecLoaderService('/path/to/spec.json', referenceTransform);
      await loader.loadSpec(); // Load first
      const spec = await loader.getSpec();

      expect(spec).toEqual(mockV3Spec);
      // Ensure loadSpec was only called once implicitly by the first await
      expect(mockConvertFileImplementation).toHaveBeenCalledTimes(1);
    });

    it('loads spec via convertFile if not already loaded', async () => {
      mockConvertFileImplementation.mockResolvedValue(mockS2OResult);
      const loader = new SpecLoaderService('/path/to/spec.json', referenceTransform);
      const spec = await loader.getSpec(); // Should trigger loadSpec

      expect(mockConvertFileImplementation).toHaveBeenCalledWith(
        '/path/to/spec.json',
        expect.any(Object)
      );
      expect(spec).toEqual(mockV3Spec);
    });

    it('loads spec via convertUrl if not already loaded', async () => {
      mockConvertUrlImplementation.mockResolvedValue(mockS2OResult);
      const loader = new SpecLoaderService('http://example.com/spec.json', referenceTransform);
      const spec = await loader.getSpec(); // Should trigger loadSpec

      expect(mockConvertUrlImplementation).toHaveBeenCalledWith(
        'http://example.com/spec.json',
        expect.any(Object)
      );
      expect(spec).toEqual(mockV3Spec);
    });
  });

  describe('getTransformedSpec', () => {
    // Mock the transformer to return a distinctly modified object
    const mockTransformedSpec = {
      ...mockV3Spec,
      info: { ...mockV3Spec.info, title: 'Transformed API' },
    };

    beforeEach(() => {
      jest
        .spyOn(referenceTransform, 'transformDocument')
        .mockImplementation(() => mockTransformedSpec);
    });

    it('returns transformed spec after loading', async () => {
      mockConvertFileImplementation.mockResolvedValue(mockS2OResult);
      const loader = new SpecLoaderService('/path/to/spec.json', referenceTransform);
      const spec = await loader.getTransformedSpec({ resourceType: 'endpoint', format: 'openapi' }); // Should load then transform

      expect(mockConvertFileImplementation).toHaveBeenCalledTimes(1); // Ensure loading happened
      const transformSpy = jest.spyOn(referenceTransform, 'transformDocument');
      expect(transformSpy).toHaveBeenCalledWith(
        mockV3Spec,
        expect.objectContaining({ resourceType: 'endpoint', format: 'openapi' })
      );
      expect(spec).toEqual(mockTransformedSpec);
    });

    it('loads spec if not loaded before transforming', async () => {
      mockConvertFileImplementation.mockResolvedValue(mockS2OResult);
      const loader = new SpecLoaderService('/path/to/spec.json', referenceTransform);
      await loader.getTransformedSpec({ resourceType: 'endpoint', format: 'openapi' }); // Trigger load

      expect(mockConvertFileImplementation).toHaveBeenCalledWith(
        '/path/to/spec.json',
        expect.any(Object)
      );
    });
  });
});

```

--------------------------------------------------------------------------------
/test/__tests__/unit/services/reference-transform.test.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPIV3 } from 'openapi-types';
import {
  OpenAPITransformer,
  ReferenceTransformService,
  TransformContext,
} from '../../../../src/services/reference-transform';

describe('ReferenceTransformService', () => {
  let service: ReferenceTransformService;
  let transformer: OpenAPITransformer;

  beforeEach(() => {
    service = new ReferenceTransformService();
    transformer = new OpenAPITransformer();
    service.registerTransformer('openapi', transformer);
  });

  it('throws error for unknown format', () => {
    const context: TransformContext = {
      resourceType: 'endpoint',
      format: 'unknown' as 'openapi' | 'asyncapi' | 'graphql',
    };

    expect(() => service.transformDocument({}, context)).toThrow(
      'No transformer registered for format: unknown'
    );
  });

  it('transforms document using registered transformer', () => {
    const context: TransformContext = {
      resourceType: 'endpoint',
      format: 'openapi',
      path: '/tasks',
      method: 'get',
    };

    const doc: OpenAPIV3.Document = {
      openapi: '3.0.0',
      info: {
        title: 'Test API',
        version: '1.0.0',
      },
      paths: {
        '/tasks': {
          get: {
            responses: {
              '200': {
                description: 'Success',
                content: {
                  'application/json': {
                    schema: {
                      $ref: '#/components/schemas/Task',
                    },
                  },
                },
              },
            },
          },
        },
      },
    };

    const result = service.transformDocument(doc, context);
    const operation = result.paths?.['/tasks']?.get;
    const response = operation?.responses?.['200'];
    expect(response).toBeDefined();
    expect('content' in response!).toBeTruthy();
    const responseObj = response! as OpenAPIV3.ResponseObject;
    expect(responseObj.content?.['application/json']?.schema).toBeDefined();
    // Expect the new format
    expect(responseObj.content!['application/json'].schema).toEqual({
      $ref: 'openapi://components/schemas/Task',
    });
  });
});

describe('OpenAPITransformer', () => {
  let transformer: OpenAPITransformer;

  beforeEach(() => {
    transformer = new OpenAPITransformer();
  });

  it('transforms schema references', () => {
    const context: TransformContext = {
      resourceType: 'endpoint',
      format: 'openapi',
    };

    const doc: OpenAPIV3.Document = {
      openapi: '3.0.0',
      info: { title: 'Test API', version: '1.0.0' },
      paths: {},
      components: {
        schemas: {
          Task: {
            $ref: '#/components/schemas/TaskId',
          },
        },
      },
    };

    const result = transformer.transformRefs(doc, context);
    // Expect the new format
    expect(result.components?.schemas?.Task).toEqual({
      $ref: 'openapi://components/schemas/TaskId',
    });
  });

  it('handles nested references', () => {
    const context: TransformContext = {
      resourceType: 'endpoint',
      format: 'openapi',
    };

    const doc: OpenAPIV3.Document = {
      openapi: '3.0.0',
      info: { title: 'Test API', version: '1.0.0' },
      paths: {
        '/tasks': {
          post: {
            requestBody: {
              required: true,
              description: 'Task creation',
              content: {
                'application/json': {
                  schema: {
                    $ref: '#/components/schemas/Task',
                  },
                },
              },
            },
            responses: {
              '201': {
                description: 'Created',
                content: {
                  'application/json': {
                    schema: {
                      $ref: '#/components/schemas/Task',
                    },
                  },
                },
              },
            },
          },
        },
      },
    };

    const result = transformer.transformRefs(doc, context);
    const taskPath = result.paths?.['/tasks'];
    expect(taskPath?.post).toBeDefined();
    const operation = taskPath!.post!;
    expect(operation.requestBody).toBeDefined();
    expect('content' in operation.requestBody!).toBeTruthy();
    const requestBody = operation.requestBody! as OpenAPIV3.RequestBodyObject;
    expect(requestBody.content?.['application/json']?.schema).toBeDefined();
    // Expect the new format
    expect(requestBody.content['application/json'].schema).toEqual({
      $ref: 'openapi://components/schemas/Task',
    });

    // Also check the response reference in the same test
    const response = operation.responses?.['201'];
    expect(response).toBeDefined();
    expect('content' in response).toBeTruthy();
    const responseObj = response as OpenAPIV3.ResponseObject;
    expect(responseObj.content?.['application/json']?.schema).toBeDefined();
    // Expect the new format
    expect(responseObj.content!['application/json'].schema).toEqual({
      $ref: 'openapi://components/schemas/Task',
    });
  });

  it('keeps external references unchanged', () => {
    const context: TransformContext = {
      resourceType: 'endpoint',
      format: 'openapi',
    };

    const doc: OpenAPIV3.Document = {
      openapi: '3.0.0',
      info: { title: 'Test API', version: '1.0.0' },
      paths: {},
      components: {
        schemas: {
          Task: {
            $ref: 'https://example.com/schemas/Task',
          },
        },
      },
    };

    const result = transformer.transformRefs(doc, context);
    const task = result.components?.schemas?.Task as OpenAPIV3.ReferenceObject;
    expect(task).toEqual({
      $ref: 'https://example.com/schemas/Task',
    });
  });

  // This test is now invalid as non-schema internal refs *should* be transformed
  // it('keeps non-schema internal references unchanged', () => { ... });

  it('transforms parameter references', () => {
    const context: TransformContext = {
      resourceType: 'endpoint',
      format: 'openapi',
    };
    const doc: OpenAPIV3.Document = {
      openapi: '3.0.0',
      info: { title: 'Test API', version: '1.0.0' },
      paths: {},
      components: {
        parameters: {
          UserIdParam: {
            $ref: '#/components/parameters/UserId', // Reference another parameter
          },
        },
      },
    };
    const result = transformer.transformRefs(doc, context);
    expect(result.components?.parameters?.UserIdParam).toEqual({
      $ref: 'openapi://components/parameters/UserId', // Expect transformation
    });
  });

  it('transforms response references', () => {
    const context: TransformContext = {
      resourceType: 'endpoint',
      format: 'openapi',
    };
    const doc: OpenAPIV3.Document = {
      openapi: '3.0.0',
      info: { title: 'Test API', version: '1.0.0' },
      paths: {},
      components: {
        responses: {
          GenericError: {
            $ref: '#/components/responses/ErrorModel', // Reference another response
          },
        },
      },
    };
    const result = transformer.transformRefs(doc, context);
    expect(result.components?.responses?.GenericError).toEqual({
      $ref: 'openapi://components/responses/ErrorModel', // Expect transformation
    });
  });

  // Add tests for other component types if needed (examples, requestBodies, etc.)

  it('handles arrays properly', () => {
    const context: TransformContext = {
      resourceType: 'endpoint',
      format: 'openapi',
    };

    const doc: OpenAPIV3.Document = {
      openapi: '3.0.0',
      info: { title: 'Test API', version: '1.0.0' },
      paths: {},
      components: {
        schemas: {
          TaskList: {
            type: 'object',
            properties: {
              items: {
                type: 'array',
                items: {
                  $ref: '#/components/schemas/Task',
                },
              },
            },
          },
        },
      },
    };

    const result = transformer.transformRefs(doc, context);
    const schema = result.components?.schemas?.TaskList;
    expect(schema).toBeDefined();
    expect('properties' in schema!).toBeTruthy();
    const schemaObject = schema! as OpenAPIV3.SchemaObject;
    expect(schemaObject.properties?.items).toBeDefined();
    const arraySchema = schemaObject.properties!.items as OpenAPIV3.ArraySchemaObject;
    // Expect the new format
    expect(arraySchema.items).toEqual({
      $ref: 'openapi://components/schemas/Task',
    });
  });

  it('preserves non-reference values', () => {
    const context: TransformContext = {
      resourceType: 'endpoint',
      format: 'openapi',
    };

    const doc: OpenAPIV3.Document = {
      openapi: '3.0.0',
      info: {
        title: 'Test API',
        version: '1.0.0',
      },
      paths: {},
      components: {
        schemas: {
          Test: {
            type: 'object',
            properties: {
              name: { type: 'string' },
            },
          },
        },
      },
    };

    const result = transformer.transformRefs(doc, context);
    expect(result).toEqual(doc);
  });
});

```

--------------------------------------------------------------------------------
/src/handlers/handler-utils.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPIV3 } from 'openapi-types';
import { RenderContext, RenderResultItem } from '../rendering/types.js'; // Already has .js
// Remove McpError/ErrorCode import - use standard Error

// Define the structure expected for each item in the contents array
export type FormattedResultItem = {
  uri: string;
  mimeType?: string;
  text: string;
  isError?: boolean;
};

/**
 * Formats RenderResultItem array into an array compatible with the 'contents'
 * property of ReadResourceResultSchema (specifically TextResourceContents).
 */
export function formatResults(
  context: RenderContext,
  items: RenderResultItem[]
): FormattedResultItem[] {
  // Add type check for formatter existence in context
  if (!context.formatter) {
    throw new Error('Formatter is missing in RenderContext for formatResults');
  }
  return items.map(item => {
    const uri = `${context.baseUri}${item.uriSuffix}`;
    let text: string;
    let mimeType: string;

    if (item.isError) {
      text = item.errorText || 'An unknown error occurred.';
      mimeType = 'text/plain';
    } else if (item.renderAsList) {
      text = typeof item.data === 'string' ? item.data : 'Invalid list data';
      mimeType = 'text/plain';
    } else {
      // Detail view: format using the provided formatter
      try {
        text = context.formatter.format(item.data);
        mimeType = context.formatter.getMimeType();
      } catch (formatError: unknown) {
        text = `Error formatting data for ${uri}: ${
          formatError instanceof Error ? formatError.message : String(formatError)
        }`;
        mimeType = 'text/plain';
        // Ensure isError is true if formatting fails
        item.isError = true;
        item.errorText = text; // Store the formatting error message
      }
    }

    // Construct the final object, prioritizing item.isError
    const finalItem: FormattedResultItem = {
      uri: uri,
      mimeType: mimeType,
      text: item.isError ? item.errorText || 'An unknown error occurred.' : text,
      isError: item.isError ?? false, // Default to false if not explicitly set
    };
    // Ensure mimeType is text/plain for errors
    if (finalItem.isError) {
      finalItem.mimeType = 'text/plain';
    }

    return finalItem;
  });
}

/**
 * Type guard to check if an object is an OpenAPIV3.Document.
 */
export function isOpenAPIV3(spec: unknown): spec is OpenAPIV3.Document {
  return (
    typeof spec === 'object' &&
    spec !== null &&
    'openapi' in spec &&
    typeof (spec as { openapi: unknown }).openapi === 'string' &&
    (spec as { openapi: string }).openapi.startsWith('3.')
  );
}

/**
 * Safely retrieves a PathItemObject from the specification using a Map.
 * Throws an McpError if the path is not found.
 *
 * @param spec The OpenAPIV3 Document.
 * @param path The decoded path string (e.g., '/users/{id}').
 * @returns The validated PathItemObject.
 * @throws {McpError} If the path is not found in spec.paths.
 */
export function getValidatedPathItem(
  spec: OpenAPIV3.Document,
  path: string
): OpenAPIV3.PathItemObject {
  if (!spec.paths) {
    // Use standard Error
    throw new Error('Specification does not contain any paths.');
  }
  const pathsMap = new Map(Object.entries(spec.paths));
  const pathItem = pathsMap.get(path);

  if (!pathItem) {
    const errorMessage = `Path "${path}" not found in the specification.`;
    // Use standard Error
    throw new Error(errorMessage);
  }
  // We assume the spec structure is valid if the key exists
  return pathItem as OpenAPIV3.PathItemObject;
}

/**
 * Validates requested HTTP methods against a PathItemObject using a Map.
 * Returns the list of valid requested methods.
 * Throws an McpError if none of the requested methods are valid for the path item.
 *
 * @param pathItem The PathItemObject to check against.
 * @param requestedMethods An array of lowercase HTTP methods requested by the user.
 * @param pathForError The path string, used for creating informative error messages.
 * @returns An array of the requested methods that are valid for this path item.
 * @throws {McpError} If none of the requested methods are valid.
 */
export function getValidatedOperations(
  pathItem: OpenAPIV3.PathItemObject,
  requestedMethods: string[],
  pathForError: string
): string[] {
  const operationsMap = new Map<string, OpenAPIV3.OperationObject>();
  Object.entries(pathItem).forEach(([method, operation]) => {
    // Check if the key is a standard HTTP method before adding
    if (
      ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'].includes(
        method.toLowerCase()
      )
    ) {
      operationsMap.set(method.toLowerCase(), operation as OpenAPIV3.OperationObject);
    }
  });

  // Validate using lowercase versions, but preserve original case for return
  const requestedMethodsLower = requestedMethods.map(m => m.toLowerCase());
  const validLowerMethods = requestedMethodsLower.filter(m => operationsMap.has(m));

  if (validLowerMethods.length === 0) {
    const availableMethods = Array.from(operationsMap.keys()).join(', ');
    // Show original case in error message for clarity
    const errorMessage = `None of the requested methods (${requestedMethods.join(', ')}) are valid for path "${pathForError}". Available methods: ${availableMethods}`;
    // Use standard Error
    throw new Error(errorMessage);
  }

  // Return the methods from the *original* requestedMethods array
  // that correspond to the valid lowercase methods found.
  return requestedMethods.filter(m => validLowerMethods.includes(m.toLowerCase()));
}

/**
 * Safely retrieves the component map for a specific type (e.g., schemas, responses)
 * from the specification using a Map.
 * Throws an McpError if spec.components or the specific type map is not found.
 *
 * @param spec The OpenAPIV3 Document.
 * @param type The ComponentType string (e.g., 'schemas', 'responses').
 * @returns The validated component map object (e.g., spec.components.schemas).
 * @throws {McpError} If spec.components or the type map is not found.
 */
export function getValidatedComponentMap(
  spec: OpenAPIV3.Document,
  type: string // Keep as string for validation flexibility
): NonNullable<OpenAPIV3.ComponentsObject[keyof OpenAPIV3.ComponentsObject]> {
  if (!spec.components) {
    // Use standard Error
    throw new Error('Specification does not contain a components section.');
  }
  // Validate the requested type against the actual keys in spec.components
  const componentsMap = new Map(Object.entries(spec.components));
  // Add type assertion for clarity, although the check below handles undefined
  const componentMapObj = componentsMap.get(type) as
    | OpenAPIV3.ComponentsObject[keyof OpenAPIV3.ComponentsObject]
    | undefined;

  if (!componentMapObj) {
    const availableTypes = Array.from(componentsMap.keys()).join(', ');
    const errorMessage = `Component type "${type}" not found in the specification. Available types: ${availableTypes}`;
    // Use standard Error
    throw new Error(errorMessage);
  }
  // We assume the spec structure is valid if the key exists
  return componentMapObj as NonNullable<
    OpenAPIV3.ComponentsObject[keyof OpenAPIV3.ComponentsObject]
  >;
}

/**
 * Validates requested component names against a specific component map (e.g., schemas).
 * Returns an array of objects containing the valid name and its corresponding detail object.
 * Throws an McpError if none of the requested names are valid for the component map.
 *
 * @param componentMap The specific component map object (e.g., spec.components.schemas).
 * @param requestedNames An array of component names requested by the user.
 * @param componentTypeForError The component type string, used for creating informative error messages.
 * @param detailsMap A Map created from the specific component map object (e.g., new Map(Object.entries(spec.components.schemas))).
 * @param requestedNames An array of component names requested by the user.
 * @param componentTypeForError The component type string, used for creating informative error messages.
 * @returns An array of { name: string, detail: V } for valid requested names, where V is the value type of the Map.
 * @throws {McpError} If none of the requested names are valid.
 */
// Modify to accept a Map directly
export function getValidatedComponentDetails<V extends OpenAPIV3.ReferenceObject | object>(
  detailsMap: Map<string, V>, // Accept Map<string, V>
  requestedNames: string[],
  componentTypeForError: string
): { name: string; detail: V }[] {
  // No longer need to create the map inside the function
  const validDetails = requestedNames
    .map(name => {
      const detail = detailsMap.get(name); // detail will be V | undefined
      return detail ? { name, detail } : null;
    })
    // Type predicate ensures we filter out nulls and have the correct type
    .filter((item): item is { name: string; detail: V } => item !== null);

  if (validDetails.length === 0) {
    // Sort available names for deterministic error messages
    const availableNames = Array.from(detailsMap.keys()).sort().join(', ');
    const errorMessage = `None of the requested names (${requestedNames.join(', ')}) are valid for component type "${componentTypeForError}". Available names: ${availableNames}`;
    // Use standard Error
    throw new Error(errorMessage);
  }

  return validDetails;
}

```

--------------------------------------------------------------------------------
/test/__tests__/unit/handlers/component-detail-handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPIV3 } from 'openapi-types';
import { RequestId } from '@modelcontextprotocol/sdk/types.js';
import { ComponentDetailHandler } from '../../../../src/handlers/component-detail-handler';
import { SpecLoaderService } from '../../../../src/types';
import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
import { suppressExpectedConsoleError } from '../../../utils/console-helpers';

// Mocks
const mockGetTransformedSpec = jest.fn();
const mockSpecLoader: SpecLoaderService = {
  getSpec: jest.fn(),
  getTransformedSpec: mockGetTransformedSpec,
};

const mockFormatter: IFormatter = new JsonFormatter();

// Sample Data
const userSchema: OpenAPIV3.SchemaObject = {
  type: 'object',
  properties: { name: { type: 'string' } },
};
const errorSchema: OpenAPIV3.SchemaObject = {
  type: 'object',
  properties: { message: { type: 'string' } },
};
const limitParam: OpenAPIV3.ParameterObject = {
  name: 'limit',
  in: 'query',
  schema: { type: 'integer' },
};

const sampleSpec: OpenAPIV3.Document = {
  openapi: '3.0.3',
  info: { title: 'Test API', version: '1.0.0' },
  paths: {},
  components: {
    schemas: {
      User: userSchema,
      Error: errorSchema,
    },
    parameters: {
      limitParam: limitParam,
    },
    // No securitySchemes defined
  },
};

describe('ComponentDetailHandler', () => {
  let handler: ComponentDetailHandler;

  beforeEach(() => {
    handler = new ComponentDetailHandler(mockSpecLoader, mockFormatter);
    mockGetTransformedSpec.mockReset();
    mockGetTransformedSpec.mockResolvedValue(sampleSpec); // Default mock
  });

  it('should return the correct template', () => {
    const template = handler.getTemplate();
    expect(template).toBeInstanceOf(ResourceTemplate);
    expect(template.uriTemplate.toString()).toBe('openapi://components/{type}/{name*}');
  });

  describe('handleRequest', () => {
    const mockExtra = {
      signal: new AbortController().signal,
      sendNotification: jest.fn(),
      sendRequest: jest.fn(),
      requestId: 'test-request-id' as RequestId,
    };

    it('should return detail for a single valid component (schema)', async () => {
      const variables: Variables = { type: 'schemas', name: 'User' }; // Use 'name' key
      const uri = new URL('openapi://components/schemas/User');

      const result = await handler.handleRequest(uri, variables, mockExtra);

      expect(mockGetTransformedSpec).toHaveBeenCalledWith({
        resourceType: 'schema',
        format: 'openapi',
      });
      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: 'openapi://components/schemas/User',
        mimeType: 'application/json',
        text: JSON.stringify(userSchema, null, 2),
        isError: false,
      });
    });

    it('should return detail for a single valid component (parameter)', async () => {
      const variables: Variables = { type: 'parameters', name: 'limitParam' };
      const uri = new URL('openapi://components/parameters/limitParam');

      const result = await handler.handleRequest(uri, variables, mockExtra);

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: 'openapi://components/parameters/limitParam',
        mimeType: 'application/json',
        text: JSON.stringify(limitParam, null, 2),
        isError: false,
      });
    });

    it('should return details for multiple valid components (array input)', async () => {
      const variables: Variables = { type: 'schemas', name: ['User', 'Error'] }; // Use 'name' key with array
      const uri = new URL('openapi://components/schemas/User,Error'); // URI might not reflect array input

      const result = await handler.handleRequest(uri, variables, mockExtra);

      expect(result.contents).toHaveLength(2);
      expect(result.contents).toContainEqual({
        uri: 'openapi://components/schemas/User',
        mimeType: 'application/json',
        text: JSON.stringify(userSchema, null, 2),
        isError: false,
      });
      expect(result.contents).toContainEqual({
        uri: 'openapi://components/schemas/Error',
        mimeType: 'application/json',
        text: JSON.stringify(errorSchema, null, 2),
        isError: false,
      });
    });

    it('should return error for invalid component type', async () => {
      const variables: Variables = { type: 'invalidType', name: 'SomeName' };
      const uri = new URL('openapi://components/invalidType/SomeName');
      const expectedLogMessage = /Invalid component type: invalidType/;

      const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
        handler.handleRequest(uri, variables, mockExtra)
      );

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: 'openapi://components/invalidType/SomeName',
        mimeType: 'text/plain',
        text: 'Invalid component type: invalidType',
        isError: true,
      });
      expect(mockGetTransformedSpec).not.toHaveBeenCalled();
    });

    it('should return error for non-existent component type in spec', async () => {
      const variables: Variables = { type: 'securitySchemes', name: 'apiKey' };
      const uri = new URL('openapi://components/securitySchemes/apiKey');
      const expectedLogMessage = /Component type "securitySchemes" not found/;

      const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
        handler.handleRequest(uri, variables, mockExtra)
      );

      expect(result.contents).toHaveLength(1);
      // Expect the specific error message from getValidatedComponentMap
      expect(result.contents[0]).toEqual({
        uri: 'openapi://components/securitySchemes/apiKey',
        mimeType: 'text/plain',
        text: 'Component type "securitySchemes" not found in the specification. Available types: schemas, parameters',
        isError: true,
      });
    });

    it('should return error for non-existent component name', async () => {
      const variables: Variables = { type: 'schemas', name: 'NonExistent' };
      const uri = new URL('openapi://components/schemas/NonExistent');
      const expectedLogMessage = /None of the requested names \(NonExistent\) are valid/;

      const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
        handler.handleRequest(uri, variables, mockExtra)
      );

      expect(result.contents).toHaveLength(1);
      // Expect the specific error message from getValidatedComponentDetails
      expect(result.contents[0]).toEqual({
        uri: 'openapi://components/schemas/NonExistent',
        mimeType: 'text/plain',
        // Expect sorted names: Error, User
        text: 'None of the requested names (NonExistent) are valid for component type "schemas". Available names: Error, User',
        isError: true,
      });
    });

    // Remove test for mix of valid/invalid names, as getValidatedComponentDetails throws now
    // it('should handle mix of valid and invalid component names', async () => { ... });

    it('should handle empty name array', async () => {
      const variables: Variables = { type: 'schemas', name: [] };
      const uri = new URL('openapi://components/schemas/');
      const expectedLogMessage = /No valid component name specified/;

      const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
        handler.handleRequest(uri, variables, mockExtra)
      );

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: 'openapi://components/schemas/',
        mimeType: 'text/plain',
        text: 'No valid component name specified.',
        isError: true,
      });
    });

    it('should handle spec loading errors', async () => {
      const error = new Error('Spec load failed');
      mockGetTransformedSpec.mockRejectedValue(error);
      const variables: Variables = { type: 'schemas', name: 'User' };
      const uri = new URL('openapi://components/schemas/User');
      const expectedLogMessage = /Spec load failed/;

      const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
        handler.handleRequest(uri, variables, mockExtra)
      );

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: 'openapi://components/schemas/User',
        mimeType: 'text/plain',
        text: 'Spec load failed',
        isError: true,
      });
    });

    it('should handle non-OpenAPI v3 spec', async () => {
      const invalidSpec = { swagger: '2.0', info: {} };
      mockGetTransformedSpec.mockResolvedValue(invalidSpec as unknown as OpenAPIV3.Document);
      const variables: Variables = { type: 'schemas', name: 'User' };
      const uri = new URL('openapi://components/schemas/User');
      const expectedLogMessage = /Only OpenAPI v3 specifications are supported/;

      const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
        handler.handleRequest(uri, variables, mockExtra)
      );

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0]).toEqual({
        uri: 'openapi://components/schemas/User',
        mimeType: 'text/plain',
        text: 'Only OpenAPI v3 specifications are supported',
        isError: true,
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/memory-bank/progress.md:
--------------------------------------------------------------------------------

```markdown
# Project Progress

## Completed Features

### Core Refactoring & New Resource Structure (✓)

1.  **Unified URI Structure:** Implemented a consistent URI structure based on OpenAPI spec hierarchy:
    - `openapi://{field}`: Access top-level fields (info, servers, tags) or list paths/component types.
    - `openapi://paths/{path}`: List methods for a specific path.
    - `openapi://paths/{path}/{method*}`: Get details for one or more operations.
    - `openapi://components/{type}`: List names for a specific component type.
    - `openapi://components/{type}/{name*}`: Get details for one or more components.
2.  **OOP Rendering Layer:** Introduced `Renderable*` classes (`RenderableDocument`, `RenderablePaths`, `RenderablePathItem`, `RenderableComponents`, `RenderableComponentMap`) to encapsulate rendering logic.
    - Uses `RenderContext` and intermediate `RenderResultItem` structure.
    - Supports token-efficient text lists and formatted detail views (JSON/YAML).
3.  **Refactored Handlers:** Created new, focused handlers for each URI pattern:
    - `TopLevelFieldHandler`
    - `PathItemHandler`
    - `OperationHandler`
    - `ComponentMapHandler`
    - `ComponentDetailHandler`
    - Uses shared utilities (`handler-utils.ts`).
4.  **Multi-Value Support:** Correctly handles `*` variables (`method*`, `name*`) passed as arrays by the SDK.
5.  **Testing:**
    - Added unit tests for all new `Renderable*` classes.
    - Added unit tests for all new handler classes.
    - Added E2E tests covering the new URI structure and functionality using `complex-endpoint.json`.
6.  **Archived Old Code:** Moved previous handler/test implementations to `local-docs/old-implementation/`.

### Previous Features (Now Integrated/Superseded)

- Schema Listing (Superseded by `openapi://components/schemas`)
- Schema Details (Superseded by `openapi://components/schemas/{name*}`)
- Endpoint Details (Superseded by `openapi://paths/{path}/{method*}`)
- Endpoint List (Superseded by `openapi://paths`)

### Remote Spec & Swagger v2.0 Support (✓)

1.  **Remote Loading:** Added support for loading specifications via HTTP/HTTPS URLs using `swagger2openapi`.
2.  **Swagger v2.0 Conversion:** Added support for automatically converting Swagger v2.0 specifications to OpenAPI v3.0 using `swagger2openapi`.
3.  **Dependency Change:** Replaced `@apidevtools/swagger-parser` with `swagger2openapi` for loading and conversion.
4.  **Configuration Update:** Confirmed and documented that configuration uses CLI arguments (`<path-or-url-to-spec>`) instead of environment variables.
5.  **Testing:**
    - Updated `SpecLoaderService` unit tests to mock `swagger2openapi` and cover new scenarios (local/remote, v2/v3).
    - Created new E2E test file (`spec-loading.test.ts`) to verify loading from local v2 and remote v3 sources.
    - Added v2.0 test fixture (`sample-v2-api.json`).

### Docker Support (✓)

1.  **Dockerfile:** Added a multi-stage production `Dockerfile` at the root. Moved the devcontainer Dockerfile to `.devcontainer/`.
2.  **Release Integration:** Added `@codedependant/semantic-release-docker` plugin to `.releaserc.json` to automate Docker image building and publishing to Docker Hub (`kadykov/mcp-openapi-schema-explorer`).
3.  **CI Workflow:** Updated the `release` job in `.github/workflows/ci.yml` to set up Docker environment (QEMU, Buildx, Login) and use `cycjimmy/semantic-release-action@v4` with the Docker plugin included in `extra_plugins`. Removed redundant Node setup/install steps from the release job.
4.  **Documentation:** Updated `README.md` with instructions and examples for running the server via Docker.

## Technical Features (✓)

### Codebase Organization (Updated)

1. File Structure

   - `src/handlers/`: Contains individual handlers and `handler-utils.ts`.
   - `src/rendering/`: Contains `Renderable*` classes, `types.ts`, `utils.ts`.
   - `src/services/`: Updated `spec-loader.ts` to use `swagger2openapi`. Added `formatters.ts`.
   - `src/`: `index.ts`, `config.ts`, `types.ts`.
   - `test/`: Updated unit tests (`spec-loader.test.ts`, `formatters.test.ts`), added new E2E test (`spec-loading.test.ts`), added v2 fixture. Updated `format.test.ts`. Updated `mcp-test-helpers.ts`.
   - `local-docs/old-implementation/`: Archived previous code.

2. Testing Structure

   - Unit tests for rendering classes (`test/__tests__/unit/rendering/`).
   - Unit tests for handlers (`test/__tests__/unit/handlers/`).
   - Unit tests for services (`spec-loader.test.ts`, `reference-transform.test.ts`, `formatters.test.ts`).
   - E2E tests (`refactored-resources.test.ts`, `spec-loading.test.ts`, `format.test.ts`). Added tests for `json-minified`.
   - Fixtures (`test/fixtures/`, including v2 and v3).
   - Test utils (`test/utils/`). Updated `StartServerOptions` type.

3. Type System

   - OpenAPI v3 types.
   - `RenderableSpecObject`, `RenderContext`, `RenderResultItem` interfaces.
   - `FormattedResultItem` type for handler results.
   - `OutputFormat` type updated.
   - `IFormatter` interface.

4. Error Handling
   - Consistent error handling via `createErrorResult` and `formatResults`.
   - Errors formatted as `text/plain`.

### Reference Transformation (✓ - Updated)

- Centralized URI generation logic in `src/utils/uri-builder.ts`.
- `ReferenceTransformService` now correctly transforms all `#/components/...` refs to `openapi://components/{type}/{name}` using the URI builder.
- Path encoding corrected to remove leading slashes before encoding.
- Unit tests updated and passing for URI builder and transformer.

### Output Format Enhancement (✓)

- Added `json-minified` output format option (`--output-format json-minified`).
- Implemented `MinifiedJsonFormatter` in `src/services/formatters.ts`.
- Updated configuration (`src/config.ts`) to accept the new format.
- Added unit tests for the new formatter (`test/__tests__/unit/services/formatters.test.ts`).
- Added E2E tests (`test/__tests__/e2e/format.test.ts`) to verify the new format.
- Updated test helper types (`test/utils/mcp-test-helpers.ts`).

### Dynamic Server Name (✓)

- Modified `src/index.ts` to load the OpenAPI spec before server initialization.
- Extracted `info.title` from the loaded spec.
- Set the `McpServer` name dynamically using the format `Schema Explorer for {title}` with a fallback.

### Dependency Cleanup & Release Automation (✓)

1.  **Dependency Correction:** Correctly categorized runtime (`swagger2openapi`) vs. development (`@types/*`) dependencies in `package.json`. Removed unused types.
2.  **Automated Releases:** Implemented `semantic-release` with conventional commit analysis, changelog generation, npm publishing, and GitHub releases.
3.  **Dynamic Versioning:** Server version is now dynamically injected via `src/version.ts`, which is generated during the release process by `semantic-release` using `scripts/generate-version.js`. A default version file is tracked in Git for local builds.
4.  **CI Workflow:** Updated `.github/workflows/ci.yml` to use Node 22, use `just` for running checks (`just all`, `just security`), and includes a `release` job using `cycjimmy/semantic-release-action@v4` to automate npm and Docker Hub publishing on pushes to `main`.

## Planned Features (⏳)

- **Handler Unit Tests:** Complete unit tests for all new handlers (mocking services).
- **Refactor Helpers:** Move duplicated helpers (`formatResults`, `isOpenAPIV3`) from handlers to `handler-utils.ts`. (Deferred during refactor).
- **Security Validation (✓):** Implemented Map-based validation helpers in `handler-utils.ts` and refactored handlers/rendering classes to resolve object injection warnings.
- **Completion Logic (✓):** Implemented `complete` callbacks in `ResourceTemplate` definitions within `src/index.ts` for `{field}`, `{path}`, `{method*}`, `{type}`, and conditionally for `{name*}`. Added E2E tests using `client.complete()`.
- **Reference Traversal:** Service to resolve `$ref` URIs (e.g., follow `openapi://components/schemas/Task` from an endpoint detail).
- **Enhanced Component Support:** Ensure all component types listed in `VALID_COMPONENT_TYPES` are fully handled if present in spec. (Reference transformation now supports all types).
- **Parameter Validation:** Add validation logic if needed. (Current Map-based approach handles key validation).
- **Further Token Optimizations:** Explore more ways to reduce token usage in list/detail views.
- **README Enhancements:** Add details on release process, secrets/vars setup. (Partially done).

## Technical Improvements (Ongoing)

1. Code Quality

   - OOP design for rendering.
   - Clear separation of concerns (Rendering vs. Handling vs. Services).
   - Improved type safety in rendering/handling logic.

2. Testing

   - Unit tests added for rendering logic.
   - Unit tests updated for URI builder, reference transformer, and path item rendering.
   - E2E tests updated for new structure and complex fixture. Added tests for resource completion.
   - Unit tests for `SpecLoaderService` updated for `swagger2openapi`.
   - CI workflow updated to use `just` and includes automated release job for npm and Docker.
   - Handler unit tests updated to accommodate `@modelcontextprotocol/sdk` v1.11.0 changes (`RequestHandlerExtra` now requires `requestId`, and `RequestId` import path corrected to `@modelcontextprotocol/sdk/types.js`).

3. API Design
   - New URI structure implemented, aligned with OpenAPI spec.
   - Consistent list/detail pattern via rendering layer.
   - Server now accepts remote URLs and Swagger v2.0 specs via CLI argument.

```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; // Import ResourceTemplate
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { OpenAPI } from 'openapi-types'; // Import OpenAPIV3 as well
import { loadConfig } from './config.js';

// Import new handlers
import { TopLevelFieldHandler } from './handlers/top-level-field-handler.js';
import { PathItemHandler } from './handlers/path-item-handler.js';
import { OperationHandler } from './handlers/operation-handler.js';
import { ComponentMapHandler } from './handlers/component-map-handler.js';
import { ComponentDetailHandler } from './handlers/component-detail-handler.js';
import { OpenAPITransformer, ReferenceTransformService } from './services/reference-transform.js';
import { SpecLoaderService } from './services/spec-loader.js';
import { createFormatter } from './services/formatters.js';
import { encodeUriPathComponent } from './utils/uri-builder.js'; // Import specific function
import { isOpenAPIV3, getValidatedComponentMap } from './handlers/handler-utils.js'; // Import type guard and helper
import { VERSION } from './version.js'; // Import the generated version

async function main(): Promise<void> {
  try {
    // Get spec path and options from command line arguments
    const [, , specPath, ...args] = process.argv;
    const options = {
      outputFormat: args.includes('--output-format')
        ? args[args.indexOf('--output-format') + 1]
        : undefined,
    };

    // Load configuration
    const config = loadConfig(specPath, options);

    // Initialize services
    const referenceTransform = new ReferenceTransformService();
    referenceTransform.registerTransformer('openapi', new OpenAPITransformer());

    const specLoader = new SpecLoaderService(config.specPath, referenceTransform);
    await specLoader.loadSpec();

    // Get the loaded spec to extract the title
    const spec: OpenAPI.Document = await specLoader.getSpec(); // Rename back to spec
    // Get the transformed spec for use in completions
    const transformedSpec: OpenAPI.Document = await specLoader.getTransformedSpec({
      resourceType: 'schema', // Use a default context
      format: 'openapi',
    });
    const defaultServerName = 'OpenAPI Schema Explorer';
    // Use original spec for title
    const serverName = spec.info?.title
      ? `Schema Explorer for ${spec.info.title}`
      : defaultServerName;

    // Brief help content for LLMs
    const helpContent = `Use resorces/templates/list to get a list of available resources. Use openapi://paths to get a list of all endpoints.`;

    // Create MCP server with dynamic name
    const server = new McpServer(
      {
        name: serverName,
        version: VERSION, // Use the imported version
      },
      {
        instructions: helpContent,
      }
    );

    // Set up formatter and new handlers
    const formatter = createFormatter(config.outputFormat);
    const topLevelFieldHandler = new TopLevelFieldHandler(specLoader, formatter);
    const pathItemHandler = new PathItemHandler(specLoader, formatter);
    const operationHandler = new OperationHandler(specLoader, formatter);
    const componentMapHandler = new ComponentMapHandler(specLoader, formatter);
    const componentDetailHandler = new ComponentDetailHandler(specLoader, formatter);

    // --- Define Resource Templates and Register Handlers ---

    // Helper to get dynamic field list for descriptions
    const getFieldList = (): string => Object.keys(transformedSpec).join(', ');
    // Helper to get dynamic component type list for descriptions
    const getComponentTypeList = (): string => {
      if (isOpenAPIV3(transformedSpec) && transformedSpec.components) {
        return Object.keys(transformedSpec.components).join(', ');
      }
      return ''; // Return empty if no components or not V3
    };

    // 1. openapi://{field}
    const fieldTemplate = new ResourceTemplate('openapi://{field}', {
      list: undefined, // List is handled by the handler logic based on field value
      complete: {
        field: (): string[] => Object.keys(transformedSpec), // Use transformedSpec
      },
    });
    server.resource(
      'openapi-field', // Unique ID for the resource registration
      fieldTemplate,
      {
        // MimeType varies (text/plain for lists, JSON/YAML for details)
        description: `Access top-level fields like ${getFieldList()}. (e.g., openapi://info)`,
        name: 'OpenAPI Field/List', // Generic name
      },
      topLevelFieldHandler.handleRequest
    );

    // 2. openapi://paths/{path}
    const pathTemplate = new ResourceTemplate('openapi://paths/{path}', {
      list: undefined, // List is handled by the handler
      complete: {
        path: (): string[] => Object.keys(transformedSpec.paths ?? {}).map(encodeUriPathComponent), // Use imported function directly
      },
    });
    server.resource(
      'openapi-path-methods',
      pathTemplate,
      {
        mimeType: 'text/plain', // This always returns a list
        description:
          'List methods for a specific path (URL encode paths with slashes). (e.g., openapi://paths/users%2F%7Bid%7D)',
        name: 'Path Methods List',
      },
      pathItemHandler.handleRequest
    );

    // 3. openapi://paths/{path}/{method*}
    const operationTemplate = new ResourceTemplate('openapi://paths/{path}/{method*}', {
      list: undefined, // Detail view handled by handler
      complete: {
        path: (): string[] => Object.keys(transformedSpec.paths ?? {}).map(encodeUriPathComponent), // Use imported function directly
        method: (): string[] => [
          // Provide static list of common methods
          'GET',
          'POST',
          'PUT',
          'DELETE',
          'PATCH',
          'OPTIONS',
          'HEAD',
          'TRACE',
        ],
      },
    });
    server.resource(
      'openapi-operation-detail',
      operationTemplate,
      {
        mimeType: formatter.getMimeType(), // Detail view uses formatter
        description:
          'Get details for one or more operations (comma-separated). (e.g., openapi://paths/users%2F%7Bid%7D/get,post)',
        name: 'Operation Detail',
      },
      operationHandler.handleRequest
    );

    // 4. openapi://components/{type}
    const componentMapTemplate = new ResourceTemplate('openapi://components/{type}', {
      list: undefined, // List is handled by the handler
      complete: {
        type: (): string[] => {
          // Use type guard to ensure spec is V3 before accessing components
          if (isOpenAPIV3(transformedSpec)) {
            return Object.keys(transformedSpec.components ?? {});
          }
          return []; // Return empty array if not V3 (shouldn't happen ideally)
        },
      },
    });
    server.resource(
      'openapi-component-list',
      componentMapTemplate,
      {
        mimeType: 'text/plain', // This always returns a list
        description: `List components of a specific type like ${getComponentTypeList()}. (e.g., openapi://components/schemas)`,
        name: 'Component List',
      },
      componentMapHandler.handleRequest
    );

    // 5. openapi://components/{type}/{name*}
    const componentDetailTemplate = new ResourceTemplate('openapi://components/{type}/{name*}', {
      list: undefined, // Detail view handled by handler
      complete: {
        type: (): string[] => {
          // Use type guard to ensure spec is V3 before accessing components
          if (isOpenAPIV3(transformedSpec)) {
            return Object.keys(transformedSpec.components ?? {});
          }
          return []; // Return empty array if not V3
        },
        name: (): string[] => {
          // Provide names only if there's exactly one component type defined
          if (
            isOpenAPIV3(transformedSpec) &&
            transformedSpec.components &&
            Object.keys(transformedSpec.components).length === 1
          ) {
            // Get the single component type key (e.g., 'schemas')
            const componentTypeKey = Object.keys(transformedSpec.components)[0];
            // Use the helper to safely get the map
            try {
              const componentTypeMap = getValidatedComponentMap(transformedSpec, componentTypeKey);
              return Object.keys(componentTypeMap);
            } catch (error) {
              // Should not happen if key came from Object.keys, but handle defensively
              console.error(`Error getting component map for key ${componentTypeKey}:`, error);
              return [];
            }
          }
          // Otherwise, return no completions for name
          return [];
        },
      },
    });
    server.resource(
      'openapi-component-detail',
      componentDetailTemplate,
      {
        mimeType: formatter.getMimeType(), // Detail view uses formatter
        description:
          'Get details for one or more components (comma-separated). (e.g., openapi://components/schemas/User,Task)',
        name: 'Component Detail',
      },
      componentDetailHandler.handleRequest
    );

    // Start server
    const transport = new StdioServerTransport();
    await server.connect(transport);
  } catch (error) {
    console.error(
      'Failed to start server:',
      error instanceof Error ? error.message : String(error)
    );
    process.exit(1);
  }
}

// Run the server
main().catch(error => {
  console.error('Unhandled error:', error instanceof Error ? error.message : String(error));
  process.exit(1);
});

```

--------------------------------------------------------------------------------
/test/__tests__/unit/handlers/handler-utils.test.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPIV3 } from 'openapi-types';
import {
  getValidatedPathItem,
  getValidatedOperations,
  getValidatedComponentMap,
  getValidatedComponentDetails,
  // We might also test formatResults and isOpenAPIV3 if needed, but focus on new helpers first
} from '../../../../src/handlers/handler-utils.js'; // Adjust path as needed

// --- Mocks and Fixtures ---

const mockSpec: OpenAPIV3.Document = {
  openapi: '3.0.0',
  info: { title: 'Test API', version: '1.0.0' },
  paths: {
    '/users': {
      get: { responses: { '200': { description: 'OK' } } },
      post: { responses: { '201': { description: 'Created' } } },
    },
    '/users/{id}': {
      get: { responses: { '200': { description: 'OK' } } },
      delete: { responses: { '204': { description: 'No Content' } } },
      parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
    },
    '/items': {
      // Path item with no standard methods
      description: 'Collection of items',
      parameters: [],
    },
  },
  components: {
    schemas: {
      User: { type: 'object', properties: { id: { type: 'string' } } },
      Error: { type: 'object', properties: { message: { type: 'string' } } },
    },
    responses: {
      NotFound: { description: 'Resource not found' },
    },
    // Intentionally missing 'parameters' section for testing
  },
};

const mockSpecNoPaths: OpenAPIV3.Document = {
  openapi: '3.0.0',
  info: { title: 'Test API', version: '1.0.0' },
  paths: {}, // Empty paths
  components: {},
};

const mockSpecNoComponents: OpenAPIV3.Document = {
  openapi: '3.0.0',
  info: { title: 'Test API', version: '1.0.0' },
  paths: { '/ping': { get: { responses: { '200': { description: 'OK' } } } } },
  // No components section
};

// --- Tests ---

describe('Handler Utils', () => {
  // --- getValidatedPathItem ---
  describe('getValidatedPathItem', () => {
    it('should return the path item object for a valid path', () => {
      const pathItem = getValidatedPathItem(mockSpec, '/users');
      expect(pathItem).toBeDefined();
      expect(pathItem).toHaveProperty('get');
      expect(pathItem).toHaveProperty('post');
    });

    it('should return the path item object for a path with parameters', () => {
      const pathItem = getValidatedPathItem(mockSpec, '/users/{id}');
      expect(pathItem).toBeDefined();
      expect(pathItem).toHaveProperty('get');
      expect(pathItem).toHaveProperty('delete');
      expect(pathItem).toHaveProperty('parameters');
    });

    it('should throw Error if path is not found', () => {
      expect(() => getValidatedPathItem(mockSpec, '/nonexistent')).toThrow(
        new Error('Path "/nonexistent" not found in the specification.')
      );
    });

    it('should throw Error if spec has no paths object', () => {
      const specWithoutPaths = { ...mockSpec, paths: undefined };
      // @ts-expect-error - Intentionally passing spec with undefined paths to test error handling
      expect(() => getValidatedPathItem(specWithoutPaths, '/users')).toThrow(
        new Error('Specification does not contain any paths.')
      );
    });

    it('should throw Error if spec has empty paths object', () => {
      expect(() => getValidatedPathItem(mockSpecNoPaths, '/users')).toThrow(
        new Error('Path "/users" not found in the specification.')
      );
    });
  });

  // --- getValidatedOperations ---
  describe('getValidatedOperations', () => {
    const usersPathItem = mockSpec.paths['/users'] as OpenAPIV3.PathItemObject;
    const userIdPathItem = mockSpec.paths['/users/{id}'] as OpenAPIV3.PathItemObject;
    const itemsPathItem = mockSpec.paths['/items'] as OpenAPIV3.PathItemObject;

    it('should return valid requested methods when all exist', () => {
      const validMethods = getValidatedOperations(usersPathItem, ['get', 'post'], '/users');
      expect(validMethods).toEqual(['get', 'post']);
    });

    it('should return valid requested methods when some exist', () => {
      const validMethods = getValidatedOperations(usersPathItem, ['get', 'put', 'post'], '/users');
      expect(validMethods).toEqual(['get', 'post']);
    });

    it('should return valid requested methods ignoring case', () => {
      const validMethods = getValidatedOperations(usersPathItem, ['GET', 'POST'], '/users');
      // Note: the helper expects lowercase input, but the internal map uses lowercase keys
      expect(validMethods).toEqual(['GET', 'POST']); // It returns the original case of valid inputs
    });

    it('should return only the valid method when one exists', () => {
      const validMethods = getValidatedOperations(
        userIdPathItem,
        ['delete', 'patch'],
        '/users/{id}'
      );
      expect(validMethods).toEqual(['delete']);
    });

    it('should throw Error if no requested methods are valid', () => {
      expect(() => getValidatedOperations(usersPathItem, ['put', 'delete'], '/users')).toThrow(
        new Error(
          'None of the requested methods (put, delete) are valid for path "/users". Available methods: get, post'
        )
      );
    });

    it('should throw Error if requested methods array is empty', () => {
      // The calling handler should prevent this, but test the helper
      expect(() => getValidatedOperations(usersPathItem, [], '/users')).toThrow(
        new Error(
          'None of the requested methods () are valid for path "/users". Available methods: get, post'
        )
      );
    });

    it('should throw Error if path item has no valid methods', () => {
      expect(() => getValidatedOperations(itemsPathItem, ['get'], '/items')).toThrow(
        new Error(
          'None of the requested methods (get) are valid for path "/items". Available methods: '
        )
      );
    });
  });

  // --- getValidatedComponentMap ---
  describe('getValidatedComponentMap', () => {
    it('should return the component map for a valid type', () => {
      const schemasMap = getValidatedComponentMap(mockSpec, 'schemas');
      expect(schemasMap).toBeDefined();
      expect(schemasMap).toHaveProperty('User');
      expect(schemasMap).toHaveProperty('Error');
    });

    it('should return the component map for another valid type', () => {
      const responsesMap = getValidatedComponentMap(mockSpec, 'responses');
      expect(responsesMap).toBeDefined();
      expect(responsesMap).toHaveProperty('NotFound');
    });

    it('should throw Error if component type is not found', () => {
      expect(() => getValidatedComponentMap(mockSpec, 'parameters')).toThrow(
        new Error(
          'Component type "parameters" not found in the specification. Available types: schemas, responses'
        )
      );
    });

    it('should throw Error if spec has no components section', () => {
      expect(() => getValidatedComponentMap(mockSpecNoComponents, 'schemas')).toThrow(
        new Error('Specification does not contain a components section.')
      );
    });
  });

  // --- getValidatedComponentDetails ---
  describe('getValidatedComponentDetails', () => {
    const schemasMap = mockSpec.components?.schemas as Record<string, OpenAPIV3.SchemaObject>;
    const responsesMap = mockSpec.components?.responses as Record<string, OpenAPIV3.ResponseObject>;
    const detailsMapSchemas = new Map(Object.entries(schemasMap));
    const detailsMapResponses = new Map(Object.entries(responsesMap));

    it('should return details for valid requested names', () => {
      const validDetails = getValidatedComponentDetails(
        detailsMapSchemas,
        ['User', 'Error'],
        'schemas'
      );
      expect(validDetails).toHaveLength(2);
      expect(validDetails[0].name).toBe('User');
      expect(validDetails[0].detail).toEqual(schemasMap['User']);
      expect(validDetails[1].name).toBe('Error');
      expect(validDetails[1].detail).toEqual(schemasMap['Error']);
    });

    it('should return details for a single valid requested name', () => {
      const validDetails = getValidatedComponentDetails(detailsMapSchemas, ['User'], 'schemas');
      expect(validDetails).toHaveLength(1);
      expect(validDetails[0].name).toBe('User');
      expect(validDetails[0].detail).toEqual(schemasMap['User']);
    });

    it('should return only details for valid names when some are invalid', () => {
      const validDetails = getValidatedComponentDetails(
        detailsMapSchemas,
        ['User', 'NonExistent', 'Error'],
        'schemas'
      );
      expect(validDetails).toHaveLength(2);
      expect(validDetails[0].name).toBe('User');
      expect(validDetails[1].name).toBe('Error');
    });

    it('should throw Error if no requested names are valid', () => {
      expect(() =>
        getValidatedComponentDetails(detailsMapSchemas, ['NonExistent1', 'NonExistent2'], 'schemas')
      ).toThrow(
        new Error(
          // Expect sorted names: Error, User
          'None of the requested names (NonExistent1, NonExistent2) are valid for component type "schemas". Available names: Error, User'
        )
      );
    });

    it('should throw Error if requested names array is empty', () => {
      // The calling handler should prevent this, but test the helper
      expect(() => getValidatedComponentDetails(detailsMapSchemas, [], 'schemas')).toThrow(
        new Error(
          // Expect sorted names: Error, User
          'None of the requested names () are valid for component type "schemas". Available names: Error, User'
        )
      );
    });

    it('should work for other component types (responses)', () => {
      const validDetails = getValidatedComponentDetails(
        detailsMapResponses,
        ['NotFound'],
        'responses'
      );
      expect(validDetails).toHaveLength(1);
      expect(validDetails[0].name).toBe('NotFound');
      expect(validDetails[0].detail).toEqual(responsesMap['NotFound']);
    });
  });
});

```

--------------------------------------------------------------------------------
/test/__tests__/e2e/format.test.ts:
--------------------------------------------------------------------------------

```typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { startMcpServer, McpTestContext } from '../../utils/mcp-test-helpers.js'; // Import McpTestContext
import { load as yamlLoad } from 'js-yaml';
// Remove old test types/guards if not needed, or adapt them
// import { isEndpointErrorResponse } from '../../utils/test-types.js';
// import type { EndpointResponse, ResourceResponse } from '../../utils/test-types.js';
// Import specific SDK types needed
import { ReadResourceResult, TextResourceContents } from '@modelcontextprotocol/sdk/types.js';
// Generic type guard for simple object check
function isObject(obj: unknown): obj is Record<string, unknown> {
  return typeof obj === 'object' && obj !== null;
}

// Type guard to check if content is TextResourceContents
function hasTextContent(
  content: ReadResourceResult['contents'][0]
): content is TextResourceContents {
  // Check for the 'text' property specifically and ensure it's not undefined
  return content && typeof (content as TextResourceContents).text === 'string';
}

function parseJson(text: string | undefined): unknown {
  if (text === undefined) throw new Error('Cannot parse undefined text');
  return JSON.parse(text);
}

function parseYaml(text: string | undefined): unknown {
  if (text === undefined) throw new Error('Cannot parse undefined text');
  const result = yamlLoad(text);
  if (result === undefined) {
    throw new Error('Invalid YAML: parsing resulted in undefined');
  }
  return result;
}

function safeParse(text: string | undefined, format: 'json' | 'yaml'): unknown {
  try {
    return format === 'json' ? parseJson(text) : parseYaml(text);
  } catch (error) {
    throw new Error(
      `Failed to parse ${format} content: ${error instanceof Error ? error.message : String(error)}`
    );
  }
}

// Removed old parseEndpointResponse

describe('Output Format E2E', () => {
  let testContext: McpTestContext;
  let client: Client;

  afterEach(async () => {
    await testContext?.cleanup();
  });

  describe('JSON format (default)', () => {
    beforeEach(async () => {
      testContext = await startMcpServer('test/fixtures/complex-endpoint.json', {
        outputFormat: 'json',
      });
      client = testContext.client;
    });

    it('should return JSON for openapi://info', async () => {
      const result = await client.readResource({ uri: 'openapi://info' });
      expect(result.contents).toHaveLength(1);
      const content = result.contents[0];
      expect(content.mimeType).toBe('application/json');
      if (!hasTextContent(content)) throw new Error('Expected text content'); // Add guard
      expect(() => safeParse(content.text, 'json')).not.toThrow();
      const data = safeParse(content.text, 'json');
      expect(isObject(data) && data['title']).toBe('Complex Endpoint Test API'); // Use bracket notation after guard
    });

    it('should return JSON for operation detail', async () => {
      const path = encodeURIComponent('api/v1/organizations/{orgId}/projects/{projectId}/tasks');
      const result = await client.readResource({ uri: `openapi://paths/${path}/get` });
      expect(result.contents).toHaveLength(1);
      const content = result.contents[0];
      expect(content.mimeType).toBe('application/json');
      if (!hasTextContent(content)) throw new Error('Expected text content'); // Add guard
      expect(() => safeParse(content.text, 'json')).not.toThrow();
      const data = safeParse(content.text, 'json');
      expect(isObject(data) && data['operationId']).toBe('getProjectTasks'); // Use bracket notation after guard
    });

    it('should return JSON for component detail', async () => {
      const result = await client.readResource({ uri: 'openapi://components/schemas/Task' });
      expect(result.contents).toHaveLength(1);
      const content = result.contents[0];
      expect(content.mimeType).toBe('application/json');
      if (!hasTextContent(content)) throw new Error('Expected text content'); // Add guard
      expect(() => safeParse(content.text, 'json')).not.toThrow();
      const data = safeParse(content.text, 'json');
      expect(isObject(data) && data['type']).toBe('object'); // Use bracket notation after guard
      expect(
        isObject(data) &&
          isObject(data['properties']) &&
          isObject(data['properties']['id']) &&
          data['properties']['id']['type']
      ).toBe('string'); // Use bracket notation with type checking
    });
  });

  describe('YAML format', () => {
    beforeEach(async () => {
      testContext = await startMcpServer('test/fixtures/complex-endpoint.json', {
        outputFormat: 'yaml',
      });
      client = testContext.client;
    });

    it('should return YAML for openapi://info', async () => {
      const result = await client.readResource({ uri: 'openapi://info' });
      expect(result.contents).toHaveLength(1);
      const content = result.contents[0];
      expect(content.mimeType).toBe('text/yaml');
      if (!hasTextContent(content)) throw new Error('Expected text content'); // Add guard
      expect(() => safeParse(content.text, 'yaml')).not.toThrow();
      expect(content.text).toContain('title: Complex Endpoint Test API');
      expect(content.text).toMatch(/\n$/);
    });

    it('should return YAML for operation detail', async () => {
      const path = encodeURIComponent('api/v1/organizations/{orgId}/projects/{projectId}/tasks');
      const result = await client.readResource({ uri: `openapi://paths/${path}/get` });
      expect(result.contents).toHaveLength(1);
      const content = result.contents[0];
      expect(content.mimeType).toBe('text/yaml');
      if (!hasTextContent(content)) throw new Error('Expected text content'); // Add guard
      expect(() => safeParse(content.text, 'yaml')).not.toThrow();
      expect(content.text).toContain('operationId: getProjectTasks');
      expect(content.text).toMatch(/\n$/);
    });

    it('should return YAML for component detail', async () => {
      const result = await client.readResource({ uri: 'openapi://components/schemas/Task' });
      expect(result.contents).toHaveLength(1);
      const content = result.contents[0];
      expect(content.mimeType).toBe('text/yaml');
      if (!hasTextContent(content)) throw new Error('Expected text content'); // Add guard
      expect(() => safeParse(content.text, 'yaml')).not.toThrow();
      expect(content.text).toContain('type: object');
      expect(content.text).toContain('properties:');
      expect(content.text).toContain('id:');
      expect(content.text).toMatch(/\n$/);
    });

    // Note: The test for listResourceTemplates is removed as it tested old template structure.
    // We could add a new test here if needed, but the mimeType for templates isn't explicitly set anymore.

    it('should handle errors in YAML format (e.g., invalid component name)', async () => {
      const result = await client.readResource({ uri: 'openapi://components/schemas/InvalidName' });
      expect(result.contents).toHaveLength(1);
      const content = result.contents[0];
      // Errors are always text/plain, regardless of configured output format
      expect(content.mimeType).toBe('text/plain');
      expect(content.isError).toBe(true);
      if (!hasTextContent(content)) throw new Error('Expected text');
      // Updated error message from getValidatedComponentDetails with sorted names
      expect(content.text).toContain(
        'None of the requested names (InvalidName) are valid for component type "schemas". Available names: CreateTaskRequest, Task, TaskList'
      );
    });
  });

  describe('Minified JSON format', () => {
    beforeEach(async () => {
      testContext = await startMcpServer('test/fixtures/complex-endpoint.json', {
        outputFormat: 'json-minified',
      });
      client = testContext.client;
    });

    it('should return minified JSON for openapi://info', async () => {
      const result = await client.readResource({ uri: 'openapi://info' });
      expect(result.contents).toHaveLength(1);
      const content = result.contents[0];
      expect(content.mimeType).toBe('application/json');
      if (!hasTextContent(content)) throw new Error('Expected text content');
      expect(() => safeParse(content.text, 'json')).not.toThrow();
      const data = safeParse(content.text, 'json');
      expect(isObject(data) && data['title']).toBe('Complex Endpoint Test API');
      // Check for lack of pretty-printing whitespace
      expect(content.text).not.toContain('\n ');
      expect(content.text).not.toContain('  '); // Double check no indentation
    });

    it('should return minified JSON for operation detail', async () => {
      const path = encodeURIComponent('api/v1/organizations/{orgId}/projects/{projectId}/tasks');
      const result = await client.readResource({ uri: `openapi://paths/${path}/get` });
      expect(result.contents).toHaveLength(1);
      const content = result.contents[0];
      expect(content.mimeType).toBe('application/json');
      if (!hasTextContent(content)) throw new Error('Expected text content');
      expect(() => safeParse(content.text, 'json')).not.toThrow();
      const data = safeParse(content.text, 'json');
      expect(isObject(data) && data['operationId']).toBe('getProjectTasks');
      // Check for lack of pretty-printing whitespace
      expect(content.text).not.toContain('\n ');
      expect(content.text).not.toContain('  ');
    });

    it('should return minified JSON for component detail', async () => {
      const result = await client.readResource({ uri: 'openapi://components/schemas/Task' });
      expect(result.contents).toHaveLength(1);
      const content = result.contents[0];
      expect(content.mimeType).toBe('application/json');
      if (!hasTextContent(content)) throw new Error('Expected text content');
      expect(() => safeParse(content.text, 'json')).not.toThrow();
      const data = safeParse(content.text, 'json');
      expect(isObject(data) && data['type']).toBe('object');
      expect(
        isObject(data) &&
          isObject(data['properties']) &&
          isObject(data['properties']['id']) &&
          data['properties']['id']['type']
      ).toBe('string');
      // Check for lack of pretty-printing whitespace
      expect(content.text).not.toContain('\n ');
      expect(content.text).not.toContain('  ');
    });
  });
});

```

--------------------------------------------------------------------------------
/test/__tests__/unit/rendering/components.test.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAPIV3 } from 'openapi-types';
import { RenderableComponents, RenderableComponentMap } from '../../../../src/rendering/components';
import { RenderContext } from '../../../../src/rendering/types';
import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';

// Mock Formatter & Context
const mockFormatter: IFormatter = new JsonFormatter();
const mockContext: RenderContext = {
  formatter: mockFormatter,
  baseUri: 'openapi://',
};

// Sample Components Object Fixture
const sampleComponents: OpenAPIV3.ComponentsObject = {
  schemas: {
    User: { type: 'object', properties: { name: { type: 'string' } } },
    Error: { type: 'object', properties: { message: { type: 'string' } } },
  },
  parameters: {
    userIdParam: { name: 'userId', in: 'path', required: true, schema: { type: 'integer' } },
  },
  responses: {
    NotFound: { description: 'Resource not found' },
  },
  // Intentionally empty type
  examples: {},
  // Intentionally missing type
  // securitySchemes: {}
};

const emptyComponents: OpenAPIV3.ComponentsObject = {};

describe('RenderableComponents (List Types)', () => {
  it('should list available component types correctly', () => {
    const renderable = new RenderableComponents(sampleComponents);
    const result = renderable.renderList(mockContext);

    expect(result).toHaveLength(1);
    expect(result[0].uriSuffix).toBe('components');
    expect(result[0].renderAsList).toBe(true);
    expect(result[0].isError).toBeUndefined();
    expect(result[0].data).toContain('Available Component Types:');
    // Check sorted types with descriptions
    expect(result[0].data).toMatch(/-\s+examples: Reusable examples of media type payloads\n/);
    expect(result[0].data).toMatch(
      /-\s+parameters: Reusable request parameters \(query, path, header, cookie\)\n/
    );
    expect(result[0].data).toMatch(/-\s+responses: Reusable API responses\n/);
    expect(result[0].data).toMatch(/-\s+schemas: Reusable data structures \(models\)\n/);
    expect(result[0].data).not.toContain('- securitySchemes'); // Missing type
    // Check hint with example
    expect(result[0].data).toContain(
      "Hint: Use 'openapi://components/{type}' to view details for a specific component type. (e.g., openapi://components/examples)"
    );
  });

  it('should handle empty components object', () => {
    const renderable = new RenderableComponents(emptyComponents);
    const result = renderable.renderList(mockContext);
    expect(result).toHaveLength(1);
    expect(result[0]).toMatchObject({
      uriSuffix: 'components',
      isError: true, // Changed expectation: should be an error if no components
      errorText: 'No components found in the specification.',
      renderAsList: true,
    });
  });

  it('should handle components object with no valid types', () => {
    // Create object with only an extension property but no valid component types
    const invalidComponents = { 'x-custom': {} } as OpenAPIV3.ComponentsObject;
    const renderable = new RenderableComponents(invalidComponents);
    const result = renderable.renderList(mockContext);
    expect(result).toHaveLength(1);
    expect(result[0]).toMatchObject({
      uriSuffix: 'components',
      isError: true,
      errorText: 'No valid component types found.',
      renderAsList: true,
    });
  });

  it('should handle undefined components object', () => {
    const renderable = new RenderableComponents(undefined);
    const result = renderable.renderList(mockContext);
    expect(result).toHaveLength(1);
    expect(result[0]).toMatchObject({
      uriSuffix: 'components',
      isError: true,
      errorText: 'No components found in the specification.',
      renderAsList: true,
    });
  });

  it('renderDetail should delegate to renderList', () => {
    const renderable = new RenderableComponents(sampleComponents);
    const listResult = renderable.renderList(mockContext);
    const detailResult = renderable.renderDetail(mockContext);
    expect(detailResult).toEqual(listResult);
  });

  it('getComponentMap should return correct map', () => {
    const renderable = new RenderableComponents(sampleComponents);
    expect(renderable.getComponentMap('schemas')).toBe(sampleComponents.schemas);
    expect(renderable.getComponentMap('parameters')).toBe(sampleComponents.parameters);
    expect(renderable.getComponentMap('examples')).toBe(sampleComponents.examples);
    expect(renderable.getComponentMap('securitySchemes')).toBeUndefined();
  });
});

describe('RenderableComponentMap (List/Detail Names)', () => {
  const schemasMap = sampleComponents.schemas;
  const parametersMap = sampleComponents.parameters;
  const emptyMap = sampleComponents.examples;
  const schemasUriSuffix = 'components/schemas';
  const paramsUriSuffix = 'components/parameters';

  describe('renderList (List Names)', () => {
    it('should list component names correctly (schemas)', () => {
      const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
      const result = renderable.renderList(mockContext);
      expect(result).toHaveLength(1);
      expect(result[0].uriSuffix).toBe(schemasUriSuffix);
      expect(result[0].renderAsList).toBe(true);
      expect(result[0].isError).toBeUndefined();
      expect(result[0].data).toContain('Available schemas:');
      expect(result[0].data).toMatch(/-\s+Error\n/); // Sorted
      expect(result[0].data).toMatch(/-\s+User\n/);
      // Check hint with example
      expect(result[0].data).toContain(
        "Hint: Use 'openapi://components/schemas/{name}' to view details for a specific schema. (e.g., openapi://components/schemas/Error)"
      );
    });

    it('should list component names correctly (parameters)', () => {
      const renderable = new RenderableComponentMap(parametersMap, 'parameters', paramsUriSuffix);
      const result = renderable.renderList(mockContext);
      expect(result).toHaveLength(1);
      expect(result[0].uriSuffix).toBe(paramsUriSuffix);
      expect(result[0].data).toContain('Available parameters:');
      expect(result[0].data).toMatch(/-\s+userIdParam\n/);
      // Check hint with example
      expect(result[0].data).toContain(
        "Hint: Use 'openapi://components/parameters/{name}' to view details for a specific parameter. (e.g., openapi://components/parameters/userIdParam)"
      );
    });

    it('should handle empty component map', () => {
      const renderable = new RenderableComponentMap(emptyMap, 'examples', 'components/examples');
      const result = renderable.renderList(mockContext);
      expect(result).toHaveLength(1);
      expect(result[0]).toMatchObject({
        uriSuffix: 'components/examples',
        isError: true,
        errorText: 'No components of type "examples" found.',
        renderAsList: true,
      });
    });

    it('should handle undefined component map', () => {
      const renderable = new RenderableComponentMap(
        undefined,
        'securitySchemes',
        'components/securitySchemes'
      );
      const result = renderable.renderList(mockContext);
      expect(result).toHaveLength(1);
      expect(result[0]).toMatchObject({
        uriSuffix: 'components/securitySchemes',
        isError: true,
        errorText: 'No components of type "securitySchemes" found.',
        renderAsList: true,
      });
    });
  });

  describe('renderComponentDetail (Get Component Detail)', () => {
    it('should return detail for a single valid component', () => {
      const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
      const result = renderable.renderComponentDetail(mockContext, ['User']);
      expect(result).toHaveLength(1);
      expect(result[0]).toEqual({
        uriSuffix: `${schemasUriSuffix}/User`,
        data: schemasMap?.User, // Expect raw component object
      });
    });

    it('should return details for multiple valid components', () => {
      const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
      const result = renderable.renderComponentDetail(mockContext, ['Error', 'User']);
      expect(result).toHaveLength(2);
      expect(result).toContainEqual({
        uriSuffix: `${schemasUriSuffix}/Error`,
        data: schemasMap?.Error,
      });
      expect(result).toContainEqual({
        uriSuffix: `${schemasUriSuffix}/User`,
        data: schemasMap?.User,
      });
    });

    it('should return error for non-existent component', () => {
      const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
      const result = renderable.renderComponentDetail(mockContext, ['NonExistent']);
      expect(result).toHaveLength(1);
      expect(result[0]).toEqual({
        uriSuffix: `${schemasUriSuffix}/NonExistent`,
        data: null,
        isError: true,
        errorText: 'Component "NonExistent" of type "schemas" not found.',
        renderAsList: true,
      });
    });

    it('should handle mix of valid and invalid components', () => {
      const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
      const result = renderable.renderComponentDetail(mockContext, ['User', 'Invalid']);
      expect(result).toHaveLength(2);
      expect(result).toContainEqual({
        uriSuffix: `${schemasUriSuffix}/User`,
        data: schemasMap?.User,
      });
      expect(result).toContainEqual({
        uriSuffix: `${schemasUriSuffix}/Invalid`,
        data: null,
        isError: true,
        errorText: 'Component "Invalid" of type "schemas" not found.',
        renderAsList: true,
      });
    });

    it('should return error if component map is undefined', () => {
      const renderable = new RenderableComponentMap(
        undefined,
        'securitySchemes',
        'components/securitySchemes'
      );
      const result = renderable.renderComponentDetail(mockContext, ['apiKey']);
      expect(result).toHaveLength(1);
      expect(result[0]).toEqual({
        uriSuffix: 'components/securitySchemes/apiKey',
        data: null,
        isError: true,
        errorText: 'Component map for type "securitySchemes" not found.',
        renderAsList: true,
      });
    });
  });

  describe('renderDetail (Interface Method)', () => {
    it('should delegate to renderList', () => {
      const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
      const listResult = renderable.renderList(mockContext);
      const detailResult = renderable.renderDetail(mockContext);
      expect(detailResult).toEqual(listResult);
    });
  });

  describe('getComponent', () => {
    it('should return correct component object', () => {
      const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
      expect(renderable.getComponent('User')).toBe(schemasMap?.User);
      expect(renderable.getComponent('Error')).toBe(schemasMap?.Error);
    });

    it('should return undefined for non-existent component', () => {
      const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
      expect(renderable.getComponent('NonExistent')).toBeUndefined();
    });

    it('should return undefined if component map is undefined', () => {
      const renderable = new RenderableComponentMap(
        undefined,
        'securitySchemes',
        'components/securitySchemes'
      );
      expect(renderable.getComponent('apiKey')).toBeUndefined();
    });
  });
});

```

--------------------------------------------------------------------------------
/memory-bank/systemPatterns.md:
--------------------------------------------------------------------------------

```markdown
# System Patterns

## Architecture Overview

```mermaid
graph TD
    CliArg[CLI Argument (Path/URL)] --> Config[src/config.ts]
    Config --> Server[MCP Server]

    CliArg --> SpecLoader[Spec Loader Service]
    SpecLoader -- Uses --> S2OLib[swagger2openapi Lib]
    SpecLoader --> Transform[Reference Transform Service]
    Transform --> Handlers[Resource Handlers]

    Server --> Handlers

    subgraph Services
        SpecLoader
        Transform
        S2OLib
    end

    subgraph Handlers
        TopLevelFieldHandler[TopLevelField Handler (openapi://{field})]
        PathItemHandler[PathItem Handler (openapi://paths/{path})]
        OperationHandler[Operation Handler (openapi://paths/{path}/{method*})]
        ComponentMapHandler[ComponentMap Handler (openapi://components/{type})]
        ComponentDetailHandler[ComponentDetail Handler (openapi://components/{type}/{name*})]
    end

    subgraph Formatters
        JsonFormatter[Json Formatter]
        YamlFormatter[Yaml Formatter]
        MinifiedJsonFormatter[Minified Json Formatter]
    end

    subgraph Rendering (OOP)
        RenderableDocument[RenderableDocument]
        RenderablePaths[RenderablePaths]
        RenderablePathItem[RenderablePathItem]
        RenderableComponents[RenderableComponents]
        RenderableComponentMap[RenderableComponentMap]
        RenderUtils[Rendering Utils]
    end

    Handlers --> Rendering
    SpecLoader --> Rendering

    subgraph Utils
        UriBuilder[URI Builder (src/utils)]
    end

    UriBuilder --> Transform
    UriBuilder --> RenderUtils
```

## Component Structure

### Services Layer

- **SpecLoaderService (`src/services/spec-loader.ts`):**
  - Uses `swagger2openapi` library.
  - Loads specification from local file path or remote URL provided via CLI argument.
  - Handles parsing of JSON/YAML.
  - Automatically converts Swagger v2.0 specs to OpenAPI v3.0 objects.
  - Provides the resulting OpenAPI v3.0 document object.
  - Handles errors during loading/conversion.
- **ReferenceTransformService (`src/services/reference-transform.ts`):**
  - Takes the OpenAPI v3.0 document from `SpecLoaderService`.
  - Traverses the document and transforms internal references (e.g., `#/components/schemas/MySchema`) into MCP URIs (e.g., `openapi://components/schemas/MySchema`).
  - Uses `UriBuilder` utility for consistent URI generation.
  - Returns the transformed OpenAPI v3.0 document.
- **Formatters (`src/services/formatters.ts`):**
  - Provide implementations for different output formats (JSON, YAML, Minified JSON).
  - Used by handlers to serialize detail view responses.
  - `IFormatter` interface defines `format()` and `getMimeType()`.
  - `createFormatter` function instantiates the correct formatter based on `OutputFormat` type (`json`, `yaml`, `json-minified`).

### Rendering Layer (OOP)

- **Renderable Classes:** Wrapper classes (`RenderableDocument`, `RenderablePaths`, `RenderablePathItem`, `RenderableComponents`, `RenderableComponentMap`) implement `RenderableSpecObject` interface.
- **Interface:** `RenderableSpecObject` defines `renderList()` and `renderDetail()` methods returning `RenderResultItem[]`.
- **RenderResultItem:** Intermediate structure holding data (`unknown`), `uriSuffix`, `isError?`, `errorText?`, `renderAsList?`.
- **RenderContext:** Passed to render methods, contains `formatter` and `baseUri`.
- **Utils:** Helper functions (`getOperationSummary`, `generateListHint`, `createErrorResult`) in `src/rendering/utils.ts`. `generateListHint` now uses centralized URI builder logic.

### Handler Layer

- **Structure:** Separate handlers for each distinct URI pattern/resource type.
- **Responsibilities:**
  - Parse URI variables provided by SDK.
  - Load/retrieve the transformed spec via `SpecLoaderService`.
  - Instantiate appropriate `Renderable*` classes.
  - Invoke the correct rendering method (`renderList` or a specific detail method like `renderTopLevelFieldDetail`, `renderOperationDetail`, `renderComponentDetail`).
  - Format the `RenderResultItem[]` using `formatResults` from `src/handlers/handler-utils.ts`.
  - Construct the final `{ contents: ... }` response object.
  - Instantiate `RenderablePathItem` correctly with raw path and built suffix.
- **Handlers:**
  - `TopLevelFieldHandler`: Handles `openapi://{field}`. Delegates list rendering for `paths`/`components` to `RenderablePaths`/`RenderableComponents`. Renders details for other fields (`info`, `servers`, etc.) via `RenderableDocument.renderTopLevelFieldDetail`.
  - `PathItemHandler`: Handles `openapi://paths/{path}`. Uses `RenderablePathItem.renderList` to list methods. Instantiates `RenderablePathItem` with raw path and built suffix.
  - `OperationHandler`: Handles `openapi://paths/{path}/{method*}`. Uses `RenderablePathItem.renderOperationDetail` for operation details. Handles multi-value `method` variable. Instantiates `RenderablePathItem` with raw path and built suffix.
  - `ComponentMapHandler`: Handles `openapi://components/{type}`. Uses `RenderableComponentMap.renderList` to list component names.
  - `ComponentDetailHandler`: Handles `openapi://components/{type}/{name*}`. Uses `RenderableComponentMap.renderComponentDetail` for component details. Handles multi-value `name` variable.
- **Utils:** Shared functions (`formatResults`, `isOpenAPIV3`, `FormattedResultItem` type, validation helpers) in `src/handlers/handler-utils.ts`.

### Utilities Layer

- **URI Builder (`src/utils/uri-builder.ts`):**
  - Centralized functions for building full URIs (`openapi://...`) and URI suffixes.
  - Handles encoding of path components (removing leading slash first).
  - Used by `ReferenceTransformService` and the rendering layer (`generateListHint`, `Renderable*` classes) to ensure consistency.

### Configuration Layer (`src/config.ts`)

- Parses command-line arguments.
- Expects a single required argument: the path or URL to the specification file.
- Supports an optional `--output-format` argument (`json`, `yaml`, `json-minified`).
- Validates arguments and provides usage instructions on error.
  - Creates the `ServerConfig` object used by the server.

## Release Automation (`semantic-release`)

- **Configuration:** Defined in `.releaserc.json`.
- **Workflow:**
  1.  `@semantic-release/commit-analyzer`: Determines release type from conventional commits.
  2.  `@semantic-release/release-notes-generator`: Generates release notes.
  3.  `@semantic-release/changelog`: Updates `CHANGELOG.md`.
  4.  `@semantic-release/npm`: Updates `version` in `package.json`.
  5.  `@semantic-release/exec`: Runs `scripts/generate-version.js` to create/update `src/version.ts` with the new version.
  6.  `@semantic-release/git`: Commits `package.json`, `package-lock.json`, `CHANGELOG.md`, and `src/version.ts`. Creates Git tag.
  7.  `@codedependant/semantic-release-docker`: Builds the Docker image using `./Dockerfile` and pushes it to Docker Hub (`kadykov/mcp-openapi-schema-explorer`) with `latest` and version tags.
  8.  `@semantic-release/github`: Creates GitHub Release.
- **Trigger:** Executed by the `release` job in the GitHub Actions workflow (`.github/workflows/ci.yml`) on pushes to the `main` branch, using `cycjimmy/semantic-release-action@v4`.
- **CI Action:** The `cycjimmy/semantic-release-action` handles installing `semantic-release` and the plugins listed in its `extra_plugins` input (`@semantic-release/changelog`, `@semantic-release/exec`, `@semantic-release/git`, `@codedependant/semantic-release-docker`).
- **Docker Environment:** The CI job sets up Docker QEMU, Buildx, and logs into Docker Hub before running the semantic-release action.
- **Versioning:** The server version in `src/index.ts` is dynamically imported from the generated `src/version.ts`. A default `src/version.ts` (with `0.0.0-dev`) is kept in the repository for local builds.

## Resource Design Patterns

### URI Structure (Revised)

- Implicit List/Detail based on path depth.
- Aligned with OpenAPI specification structure.
- **Templates:**
  - `openapi://{field}`: Top-level field details (info, servers) or list trigger (paths, components).
  - `openapi://paths/{path}`: List methods for a specific path.
  - `openapi://paths/{path}/{method*}`: Operation details (supports multiple methods).
  - `openapi://components/{type}`: List names for a specific component type.
  - `openapi://components/{type}/{name*}`: Component details (supports multiple names).
- **Completions:**
  - Defined directly in `src/index.ts` within `ResourceTemplate` definitions passed to `server.resource()`.
  - Uses the `transformedSpec` object loaded before server initialization.
  - Provides suggestions for `{field}`, `{path}`, `{method*}`, `{type}`.
  - Provides suggestions for `{name*}` _only_ if the spec contains exactly one component type.
- **Reference URIs (Corrected):**
  - Internal `$ref`s like `#/components/schemas/MySchema` are transformed by `ReferenceTransformService` into resolvable MCP URIs: `openapi://components/schemas/MySchema`.
  - This applies to all component types under `#/components/`.
  - External references remain unchanged.

### Response Format Patterns

1. **Token-Efficient Lists:**
   - `text/plain` format used for all list views (`openapi://paths`, `openapi://components`, `openapi://paths/{path}`, `openapi://components/{type}`).
   - Include hints for navigating to detail views, generated via `generateListHint` using the centralized URI builder.
   - `openapi://paths` format: `METHOD1 METHOD2 /path`
   - `openapi://paths/{path}` format: `METHOD: Summary/OpId`
   - `openapi://components` format: `- type`
   - `openapi://components/{type}` format: `- name`
2. **Detail Views:**
   - Use configured formatter (JSON/YAML/Minified JSON via `IFormatter`).
   - Handled by `openapi://{field}` (for non-structural fields), `openapi://paths/{path}/{method*}`, `openapi://components/{type}/{name*}`.
3. **Error Handling:**
   - Handlers catch errors and use `createErrorResult` utility.
   - `formatResults` utility formats errors into `FormattedResultItem` with `isError: true`, `mimeType: 'text/plain'`, and error message in `text`.
4. **Type Safety:**
   - Strong typing with OpenAPI v3 types.
   - `Renderable*` classes encapsulate type-specific logic.
   - `isOpenAPIV3` type guard used in handlers.

## Extension Points

1. Reference Transformers:

   - AsyncAPI transformer
   - GraphQL transformer
   - Custom format transformers

2. Resource Handlers:

   - Schema resource handler
   - Additional reference handlers
   - Custom format handlers (via `IFormatter` interface)

3. URI Resolution:

   - Reference transformation service (`ReferenceTransformService`) handles converting `#/components/{type}/{name}` to `openapi://components/{type}/{name}` URIs during spec loading.
   - Cross-resource linking is implicit via generated URIs in hints and transformed refs.
   - External references are currently kept as-is.

4. Validation:
   - Parameter validation
   - Reference validation
   - Format-specific validation

## Testing Strategy

1. **Unit Tests:**
   - `SpecLoaderService`: Mock `swagger2openapi` to test local/remote and v2/v3 loading logic, including error handling.
   - `ReferenceTransformService`: Verify correct transformation of `#/components/...` refs to MCP URIs.
   - Rendering Classes (`Renderable*`): Test list and detail rendering logic.
   - Handlers: Mock services (`SpecLoaderService`, `IFormatter`) to test URI parsing and delegation to rendering classes.
   - `UriBuilder`: Test URI encoding and generation.
2. **E2E Tests:**
   - Use `mcp-test-helpers` to start the server with different spec inputs.
   - **`spec-loading.test.ts`:** Verify basic resource access (`info`, `paths`, `components`, specific component detail) works correctly when loading:
     - Local Swagger v2.0 spec (`test/fixtures/sample-v2-api.json`).
     - Remote OpenAPI v3.0 spec (e.g., Petstore URL).
   - **`refactored-resources.test.ts`:** Continue to test detailed resource interactions (multi-value params, specific path/method/component combinations, errors) using the primary complex local v3 fixture (`complex-endpoint.json`).
   - **`format.test.ts`:** Verify different output formats (JSON/YAML/Minified JSON) work as expected.
   - **Completion Tests:** Added to `refactored-resources.test.ts` using `client.complete()` to verify completion logic.
3. **Test Support:**
   - Type-safe test utilities (`mcp-test-helpers`). Updated `StartServerOptions` to include `json-minified`.
   - Test fixtures for v2.0 and v3.0 specs.
4. **CI Integration (`.github/workflows/ci.yml`):**
   - **`test` Job:** Runs on push/PR to `main`. Uses Node 22, installs `just`, runs `npm ci`, then `just all` (format, lint, build, test). Uploads coverage.
   - **`security` Job:** Runs on push/PR to `main`. Uses Node 22, installs `just`, runs `npm ci`, then `just security` (audit, licenses). Runs CodeQL analysis separately.
   - **`release` Job:** Runs _only_ on push to `main` after `test` and `security` pass. Checks out full history, sets up Docker (QEMU, Buildx, Login), then runs `cycjimmy/semantic-release-action@v4` with necessary `extra_plugins` and environment variables (`GITHUB_TOKEN`, `NPM_TOKEN`, Docker Hub credentials handled by login action).

```

--------------------------------------------------------------------------------
/test/__tests__/e2e/resources.test.ts:
--------------------------------------------------------------------------------

```typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
// Import specific SDK types needed
import {
  ReadResourceResult,
  TextResourceContents,
  // Removed unused CompleteRequest, CompleteResult
} from '@modelcontextprotocol/sdk/types.js';
import { startMcpServer, McpTestContext } from '../../utils/mcp-test-helpers';
import path from 'path';

// Use the complex spec for E2E tests
const complexSpecPath = path.resolve(__dirname, '../../fixtures/complex-endpoint.json');

// Helper function to parse JSON safely
function parseJsonSafely(text: string | undefined): unknown {
  if (text === undefined) {
    throw new Error('Received undefined text for JSON parsing');
  }
  try {
    return JSON.parse(text);
  } catch (e) {
    console.error('Failed to parse JSON:', text);
    throw new Error(`Invalid JSON received: ${e instanceof Error ? e.message : String(e)}`);
  }
}

// Type guard to check if content is TextResourceContents
function hasTextContent(
  content: ReadResourceResult['contents'][0]
): content is TextResourceContents {
  // Check for the 'text' property specifically, differentiating from BlobResourceContents
  return typeof (content as TextResourceContents).text === 'string';
}

describe('E2E Tests for Refactored Resources', () => {
  let testContext: McpTestContext;
  let client: Client; // Use the correct Client type

  // Helper to setup client for tests
  async function setup(specPath: string = complexSpecPath): Promise<void> {
    // Use complex spec by default
    testContext = await startMcpServer(specPath, { outputFormat: 'json' }); // Default to JSON
    client = testContext.client; // Get client from helper context
    // Initialization is handled by startMcpServer connecting the transport
  }

  afterEach(async () => {
    await testContext?.cleanup(); // Use cleanup function from helper
  });

  // Helper to read resource and perform basic checks
  async function readResourceAndCheck(uri: string): Promise<ReadResourceResult['contents'][0]> {
    const result = await client.readResource({ uri });
    expect(result.contents).toHaveLength(1);
    const content = result.contents[0];
    expect(content.uri).toBe(uri);
    return content;
  }

  // Helper to read resource and check for text/plain list content
  async function checkTextListResponse(uri: string, expectedSubstrings: string[]): Promise<string> {
    const content = await readResourceAndCheck(uri);
    expect(content.mimeType).toBe('text/plain');
    expect(content.isError).toBeFalsy();
    if (!hasTextContent(content)) throw new Error('Expected text content');
    for (const sub of expectedSubstrings) {
      expect(content.text).toContain(sub);
    }
    return content.text;
  }

  // Helper to read resource and check for JSON detail content
  async function checkJsonDetailResponse(uri: string, expectedObject: object): Promise<unknown> {
    const content = await readResourceAndCheck(uri);
    expect(content.mimeType).toBe('application/json');
    expect(content.isError).toBeFalsy();
    if (!hasTextContent(content)) throw new Error('Expected text content');
    const data = parseJsonSafely(content.text);
    expect(data).toMatchObject(expectedObject);
    return data;
  }

  // Helper to read resource and check for error
  async function checkErrorResponse(uri: string, expectedErrorText: string): Promise<void> {
    const content = await readResourceAndCheck(uri);
    expect(content.isError).toBe(true);
    expect(content.mimeType).toBe('text/plain'); // Errors are plain text
    if (!hasTextContent(content)) throw new Error('Expected text content for error');
    expect(content.text).toContain(expectedErrorText);
  }

  describe('openapi://{field}', () => {
    beforeEach(async () => await setup());

    it('should retrieve the "info" field', async () => {
      // Matches complex-endpoint.json
      await checkJsonDetailResponse('openapi://info', {
        title: 'Complex Endpoint Test API',
        version: '1.0.0',
      });
    });

    it('should retrieve the "paths" list', async () => {
      // Matches complex-endpoint.json
      await checkTextListResponse('openapi://paths', [
        'Hint:',
        'GET POST /api/v1/organizations/{orgId}/projects/{projectId}/tasks',
      ]);
    });

    it('should retrieve the "components" list', async () => {
      // Matches complex-endpoint.json (only has schemas)
      await checkTextListResponse('openapi://components', [
        'Available Component Types:',
        '- schemas',
        "Hint: Use 'openapi://components/{type}'",
      ]);
    });

    it('should return error for invalid field', async () => {
      const uri = 'openapi://invalidfield';
      await checkErrorResponse(uri, 'Field "invalidfield" not found');
    });
  });

  describe('openapi://paths/{path}', () => {
    beforeEach(async () => await setup());

    it('should list methods for the complex task path', async () => {
      const complexPath = 'api/v1/organizations/{orgId}/projects/{projectId}/tasks';
      const encodedPath = encodeURIComponent(complexPath);
      // Update expected format based on METHOD: Summary/OpId
      await checkTextListResponse(`openapi://paths/${encodedPath}`, [
        "Hint: Use 'openapi://paths/api%2Fv1%2Forganizations%2F%7BorgId%7D%2Fprojects%2F%7BprojectId%7D%2Ftasks/{method}'", // Hint comes first now
        '', // Blank line after hint
        'GET: Get Tasks', // METHOD: summary
        'POST: Create Task', // METHOD: summary
      ]);
    });

    it('should return error for non-existent path', async () => {
      const encodedPath = encodeURIComponent('nonexistent');
      const uri = `openapi://paths/${encodedPath}`;
      // Updated error message from getValidatedPathItem
      await checkErrorResponse(uri, 'Path "/nonexistent" not found in the specification.');
    });
  });

  describe('openapi://paths/{path}/{method*}', () => {
    beforeEach(async () => await setup());

    it('should get details for GET on complex path', async () => {
      const complexPath = 'api/v1/organizations/{orgId}/projects/{projectId}/tasks';
      const encodedPath = encodeURIComponent(complexPath);
      // Check operationId from complex-endpoint.json
      await checkJsonDetailResponse(`openapi://paths/${encodedPath}/get`, {
        operationId: 'getProjectTasks',
      });
    });

    it('should get details for multiple methods GET,POST on complex path', async () => {
      const complexPath = 'api/v1/organizations/{orgId}/projects/{projectId}/tasks';
      const encodedPath = encodeURIComponent(complexPath);
      const result = await client.readResource({ uri: `openapi://paths/${encodedPath}/get,post` });
      expect(result.contents).toHaveLength(2);

      const getContent = result.contents.find(c => c.uri.endsWith('/get'));
      expect(getContent).toBeDefined();
      expect(getContent?.isError).toBeFalsy();
      if (!getContent || !hasTextContent(getContent))
        throw new Error('Expected text content for GET');
      const getData = parseJsonSafely(getContent.text);
      // Check operationId from complex-endpoint.json
      expect(getData).toMatchObject({ operationId: 'getProjectTasks' });

      const postContent = result.contents.find(c => c.uri.endsWith('/post'));
      expect(postContent).toBeDefined();
      expect(postContent?.isError).toBeFalsy();
      if (!postContent || !hasTextContent(postContent))
        throw new Error('Expected text content for POST');
      const postData = parseJsonSafely(postContent.text);
      // Check operationId from complex-endpoint.json
      expect(postData).toMatchObject({ operationId: 'createProjectTask' });
    });

    it('should return error for invalid method on complex path', async () => {
      const complexPath = 'api/v1/organizations/{orgId}/projects/{projectId}/tasks';
      const encodedPath = encodeURIComponent(complexPath);
      const uri = `openapi://paths/${encodedPath}/put`;
      // Updated error message from getValidatedOperations
      await checkErrorResponse(
        uri,
        'None of the requested methods (put) are valid for path "/api/v1/organizations/{orgId}/projects/{projectId}/tasks". Available methods: get, post'
      );
    });
  });

  describe('openapi://components/{type}', () => {
    beforeEach(async () => await setup());

    it('should list schemas', async () => {
      // Matches complex-endpoint.json
      await checkTextListResponse('openapi://components/schemas', [
        'Available schemas:',
        '- CreateTaskRequest',
        '- Task',
        '- TaskList',
        "Hint: Use 'openapi://components/schemas/{name}'",
      ]);
    });

    it('should return error for invalid type', async () => {
      const uri = 'openapi://components/invalid';
      await checkErrorResponse(uri, 'Invalid component type: invalid');
    });
  });

  describe('openapi://components/{type}/{name*}', () => {
    beforeEach(async () => await setup());

    it('should get details for schema Task', async () => {
      // Matches complex-endpoint.json
      await checkJsonDetailResponse('openapi://components/schemas/Task', {
        type: 'object',
        properties: { id: { type: 'string' }, title: { type: 'string' } },
      });
    });

    it('should get details for multiple schemas Task,TaskList', async () => {
      // Matches complex-endpoint.json
      const result = await client.readResource({
        uri: 'openapi://components/schemas/Task,TaskList',
      });
      expect(result.contents).toHaveLength(2);

      const taskContent = result.contents.find(c => c.uri.endsWith('/Task'));
      expect(taskContent).toBeDefined();
      expect(taskContent?.isError).toBeFalsy();
      if (!taskContent || !hasTextContent(taskContent))
        throw new Error('Expected text content for Task');
      const taskData = parseJsonSafely(taskContent.text);
      expect(taskData).toMatchObject({ properties: { id: { type: 'string' } } });

      const taskListContent = result.contents.find(c => c.uri.endsWith('/TaskList'));
      expect(taskListContent).toBeDefined();
      expect(taskListContent?.isError).toBeFalsy();
      if (!taskListContent || !hasTextContent(taskListContent))
        throw new Error('Expected text content for TaskList');
      const taskListData = parseJsonSafely(taskListContent.text);
      expect(taskListData).toMatchObject({ properties: { items: { type: 'array' } } });
    });

    it('should return error for invalid name', async () => {
      const uri = 'openapi://components/schemas/InvalidSchemaName';
      // Updated error message from getValidatedComponentDetails with sorted names
      await checkErrorResponse(
        uri,
        'None of the requested names (InvalidSchemaName) are valid for component type "schemas". Available names: CreateTaskRequest, Task, TaskList'
      );
    });
  });

  // Removed ListResourceTemplates test suite as the 'complete' property
  // is likely not part of the standard response payload.
  // We assume the templates are registered correctly in src/index.ts.

  describe('Completion Tests', () => {
    beforeEach(async () => await setup()); // Use the same setup

    it('should provide completions for {field}', async () => {
      const params = {
        argument: { name: 'field', value: '' }, // Empty value to get all
        ref: { type: 'ref/resource' as const, uri: 'openapi://{field}' },
      };
      const result = await client.complete(params);
      expect(result.completion).toBeDefined();
      expect(result.completion.values).toEqual(
        expect.arrayContaining(['openapi', 'info', 'paths', 'components']) // Based on complex-endpoint.json
      );
      expect(result.completion.values).toHaveLength(4);
    });

    it('should provide completions for {path}', async () => {
      const params = {
        argument: { name: 'path', value: '' }, // Empty value to get all
        ref: { type: 'ref/resource' as const, uri: 'openapi://paths/{path}' },
      };
      const result = await client.complete(params);
      expect(result.completion).toBeDefined();
      // Check for the encoded path from complex-endpoint.json
      expect(result.completion.values).toEqual([
        'api%2Fv1%2Forganizations%2F%7BorgId%7D%2Fprojects%2F%7BprojectId%7D%2Ftasks',
      ]);
    });

    it('should provide completions for {method*}', async () => {
      const params = {
        argument: { name: 'method', value: '' }, // Empty value to get all
        ref: {
          type: 'ref/resource' as const,
          uri: 'openapi://paths/{path}/{method*}', // Use the exact template URI
        },
      };
      const result = await client.complete(params);
      expect(result.completion).toBeDefined();
      // Check for the static list of methods defined in src/index.ts
      expect(result.completion.values).toEqual([
        'GET',
        'POST',
        'PUT',
        'DELETE',
        'PATCH',
        'OPTIONS',
        'HEAD',
        'TRACE',
      ]);
    });

    it('should provide completions for {type}', async () => {
      const params = {
        argument: { name: 'type', value: '' }, // Empty value to get all
        ref: { type: 'ref/resource' as const, uri: 'openapi://components/{type}' },
      };
      const result = await client.complete(params);
      expect(result.completion).toBeDefined();
      // Check for component types in complex-endpoint.json
      expect(result.completion.values).toEqual(['schemas']);
    });

    // Updated test for conditional name completion
    it('should provide completions for {name*} when only one component type exists', async () => {
      // complex-endpoint.json only has 'schemas'
      const params = {
        argument: { name: 'name', value: '' },
        ref: {
          type: 'ref/resource' as const,
          uri: 'openapi://components/{type}/{name*}', // Use the exact template URI
        },
      };
      const result = await client.complete(params);
      expect(result.completion).toBeDefined();
      // Expect schema names from complex-endpoint.json
      expect(result.completion.values).toEqual(
        expect.arrayContaining(['CreateTaskRequest', 'Task', 'TaskList'])
      );
      expect(result.completion.values).toHaveLength(3);
    });

    // New test for multiple component types
    it('should NOT provide completions for {name*} when multiple component types exist', async () => {
      // Need to restart the server with the multi-component spec
      await testContext?.cleanup(); // Clean up previous server
      const multiSpecPath = path.resolve(__dirname, '../../fixtures/multi-component-types.json');
      await setup(multiSpecPath); // Restart server with new spec

      const params = {
        argument: { name: 'name', value: '' },
        ref: {
          type: 'ref/resource' as const,
          uri: 'openapi://components/{type}/{name*}', // Use the exact template URI
        },
      };
      const result = await client.complete(params);
      expect(result.completion).toBeDefined();
      // Expect empty array because multiple types (schemas, parameters) exist
      expect(result.completion.values).toEqual([]);
    });
  });
});

```
Page 2/2FirstPrevNextLast