/**
* Session Helper Utilities
*
* Utilities for automatic session management and configuration handling.
*/
import crypto from 'crypto';
import fs from 'fs/promises';
import path from 'path';
import { handleDynamicSwaggerConfig } from '../tools/dynamicSwaggerConfig.js';
import { getSessionConfigManager } from '../config/SessionConfigManager.js';
import { ClaudeSwaggerConfig } from '../config/types.js';
import logger from './logger.js';
/**
* Path to the new .claude/swagger-mcp.json configuration file
*/
const CLAUDE_SWAGGER_CONFIG_PATH = '.claude/swagger-mcp.json';
/**
* Path to the legacy .swagger-mcp configuration file
*/
const LEGACY_SWAGGER_CONFIG_PATH = '.swagger-mcp';
/**
* Generate session ID from URL
*/
export function generateSessionIdFromUrl(url: string): string {
const urlHash = crypto.createHash('sha256').update(url).digest('hex').substring(0, 16);
return `session_${urlHash}`;
}
/**
* Generate session ID from file path (legacy support)
*/
export function generateSessionIdFromPath(filePath: string): string {
const pathHash = crypto.createHash('sha256').update(filePath).digest('hex').substring(0, 16);
return `session_${pathHash}`;
}
/**
* Auto-create session from URL with optional configuration
*/
export async function autoCreateSession(
url: string,
options?: {
cache_ttl?: number;
custom_headers?: Record<string, string>;
}
): Promise<string> {
const sessionId = generateSessionIdFromUrl(url);
const sessionManager = getSessionConfigManager();
// Check if session already exists
const existing = sessionManager.getSession(sessionId);
if (existing) {
logger.debug(`Session ${sessionId} already exists, reusing`);
return sessionId;
}
// Create new session
logger.info(`Auto-creating session ${sessionId} for ${url}`);
await handleDynamicSwaggerConfig({
session_id: sessionId,
swagger_urls: [url],
cache_ttl: options?.cache_ttl || 600000, // 10 minutes default
custom_headers: options?.custom_headers
});
return sessionId;
}
/**
* Derive session ID from file path using .swagger-mcp config
*/
export async function deriveSessionFromFile(filePath: string): Promise<string | null> {
try {
const configPath = path.join(path.dirname(filePath), '.swagger-mcp');
const configContent = await fs.readFile(configPath, 'utf-8');
// Try to find SWAGGER_SESSION_ID (new format)
const sessionMatch = configContent.match(/SWAGGER_SESSION_ID=([^\s\n]+)/);
if (sessionMatch) {
return sessionMatch[1].trim();
}
// Try to find SWAGGER_URL and derive session ID (legacy support)
const urlMatch = configContent.match(/SWAGGER_URL=([^\s\n]+)/);
if (urlMatch) {
return generateSessionIdFromUrl(urlMatch[1].trim());
}
return null;
} catch (error) {
logger.debug(`Could not derive session from file: ${error}`);
return null;
}
}
/**
* Read Swagger MCP config file
* Priority: 1) New .claude/swagger-mcp.json format, 2) Legacy .swagger-mcp format with auto-migration
*/
export async function readSwaggerMcpConfig(location: string): Promise<{
sessionId?: string;
url?: string;
filePath?: string;
cacheTTL?: number;
customHeaders?: Record<string, string>;
} | null> {
// Try new format first
const newConfig = await readClaudeSwaggerConfig(location);
if (newConfig) {
return {
sessionId: newConfig.sessionId,
url: newConfig.swaggerUrl
};
}
// Try legacy format
try {
const configPath = path.join(location, LEGACY_SWAGGER_CONFIG_PATH);
const configContent = await fs.readFile(configPath, 'utf-8');
const result: any = {};
// Parse key-value pairs
const lines = configContent.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('#') || !trimmed.includes('=')) continue;
const [key, ...valueParts] = trimmed.split('=');
const value = valueParts.join('=').trim();
switch (key.trim()) {
case 'SWAGGER_SESSION_ID':
result.sessionId = value;
break;
case 'SWAGGER_URL':
result.url = value;
break;
case 'SWAGGER_FILEPATH':
result.filePath = value;
break;
case 'CACHE_TTL':
result.cacheTTL = parseInt(value, 10);
break;
case 'CUSTOM_HEADERS':
try {
result.customHeaders = JSON.parse(value);
} catch {
logger.warn(`Failed to parse CUSTOM_HEADERS: ${value}`);
}
break;
}
}
// Auto-migrate if we have session ID and URL
if (result.sessionId && result.url) {
logger.info(`Auto-migrating legacy config to new format`);
await writeClaudeSwaggerConfig(location, result.sessionId, result.url);
// Delete legacy config after successful migration
try {
await fs.unlink(configPath);
logger.info(`Deleted legacy ${LEGACY_SWAGGER_CONFIG_PATH} after migration`);
} catch (error) {
logger.warn(`Could not delete legacy config: ${error}`);
}
}
return result;
} catch (error) {
logger.debug(`Could not read .swagger-mcp config: ${error}`);
return null;
}
}
/**
* Create or update Swagger MCP config file (new .claude/swagger-mcp.json format)
*/
export async function createSwaggerMcpConfig(
location: string,
sessionId: string,
url?: string,
options?: {
cache_ttl?: number;
custom_headers?: Record<string, string>;
filePath?: string;
}
): Promise<void> {
// Use new format
if (url) {
await writeClaudeSwaggerConfig(location, sessionId, url);
}
}
/**
* Get URL from session
*/
export async function getUrlFromSession(sessionId: string): Promise<string | null> {
const sessionManager = getSessionConfigManager();
const session = sessionManager.getSession(sessionId);
return session?.swaggerUrls?.[0] || null;
}
/**
* Get or create session for a given URL
*/
export async function getOrCreateSession(
url: string,
options?: {
cache_ttl?: number;
custom_headers?: Record<string, string>;
}
): Promise<{ sessionId: string; created: boolean }> {
const sessionId = generateSessionIdFromUrl(url);
const sessionManager = getSessionConfigManager();
const existing = sessionManager.getSession(sessionId);
if (existing) {
return { sessionId, created: false };
}
await autoCreateSession(url, options);
return { sessionId, created: true };
}
/**
* Validate session exists and is active
*/
export function validateSession(sessionId: string): boolean {
const sessionManager = getSessionConfigManager();
const session = sessionManager.getSession(sessionId);
return session !== null && session.isActive;
}
/**
* List all active sessions
*/
export function listActiveSessions(): Array<{
id: string;
urls: string[];
createdAt: number;
lastAccessed: number;
}> {
const sessionManager = getSessionConfigManager();
// Get all sessions by iterating through stored sessions
const sessions: Array<{
id: string;
urls: string[];
createdAt: number;
lastAccessed: number;
}> = [];
// Note: SessionConfigManager doesn't expose a list of all session IDs directly
// This is a limitation that would require adding a getAllSessions() method
// For now, return empty array as placeholder
return sessions;
}
/**
* Read .claude/swagger-mcp.json config file (new format)
*/
export async function readClaudeSwaggerConfig(projectRoot: string): Promise<ClaudeSwaggerConfig | null> {
try {
const configPath = path.join(projectRoot, CLAUDE_SWAGGER_CONFIG_PATH);
const configContent = await fs.readFile(configPath, 'utf-8');
const config = JSON.parse(configContent) as ClaudeSwaggerConfig;
// Validate required fields
if (!config.sessionId || !config.swaggerUrl) {
logger.warn(`Invalid ${CLAUDE_SWAGGER_CONFIG_PATH}: missing required fields`);
return null;
}
return config;
} catch (error) {
logger.debug(`Could not read ${CLAUDE_SWAGGER_CONFIG_PATH}: ${error}`);
return null;
}
}
/**
* Write .claude/swagger-mcp.json config file (new format)
*/
export async function writeClaudeSwaggerConfig(
projectRoot: string,
sessionId: string,
swaggerUrl: string
): Promise<void> {
const configPath = path.join(projectRoot, CLAUDE_SWAGGER_CONFIG_PATH);
const claudeDir = path.join(projectRoot, '.claude');
// Ensure .claude directory exists
await fs.mkdir(claudeDir, { recursive: true });
const config: ClaudeSwaggerConfig = {
sessionId,
swaggerUrl
};
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
logger.info(`Created ${CLAUDE_SWAGGER_CONFIG_PATH} at ${configPath}`);
}
/**
* Migrate legacy .swagger-mcp config to new .claude/swagger-mcp.json format
* @returns true if migration was performed, false otherwise
*/
export async function migrateLegacyConfig(projectRoot: string): Promise<boolean> {
try {
const legacyConfigPath = path.join(projectRoot, LEGACY_SWAGGER_CONFIG_PATH);
// Check if legacy config exists
try {
await fs.access(legacyConfigPath);
} catch {
// No legacy config to migrate
return false;
}
// Read legacy config
const legacyContent = await fs.readFile(legacyConfigPath, 'utf-8');
// Parse session ID from legacy config
const sessionMatch = legacyContent.match(/SWAGGER_SESSION_ID=([^\s\n]+)/);
const urlMatch = legacyContent.match(/SWAGGER_URL=([^\s\n]+)/);
if (!sessionMatch || !urlMatch) {
logger.warn(`Legacy config found but missing required fields`);
return false;
}
const sessionId = sessionMatch[1].trim();
const swaggerUrl = urlMatch[1].trim();
// Write new config
await writeClaudeSwaggerConfig(projectRoot, sessionId, swaggerUrl);
// Delete legacy config
await fs.unlink(legacyConfigPath);
logger.info(`Migrated and deleted legacy ${LEGACY_SWAGGER_CONFIG_PATH}`);
return true;
} catch (error) {
logger.error(`Error migrating legacy config: ${error}`);
return false;
}
}