Skip to main content
Glama

WhaTap MXQL CLI

by devload
ARCHITECTURE.md30.6 kB
# WhaTap MXQL CLI + MCP - 아키텍처 설계 ## 🎯 설계 철학 **CLI First, MCP as Extension** - CLI: 독립적인 도구로 먼저 개발 및 테스트 - MCP: CLI의 core 로직을 재사용하여 Claude Code와 통합 ## 📦 프로젝트 구조 ``` whatap-mxql-cli/ ├── package.json ├── tsconfig.json ├── README.md ├── .gitignore │ ├── bin/ │ ├── whatap.js # CLI 실행 파일 (#!/usr/bin/env node) │ └── whatap-mcp.js # MCP 서버 실행 파일 │ ├── src/ │ ├── core/ # 핵심 비즈니스 로직 (CLI + MCP 공유) │ │ ├── auth/ │ │ │ ├── AuthManager.ts │ │ │ └── SessionStore.ts │ │ ├── api/ │ │ │ ├── WhatapClient.ts │ │ │ └── MxqlExecutor.ts │ │ ├── validator/ │ │ │ └── MxqlValidator.ts │ │ └── types/ │ │ └── index.ts │ │ │ ├── cli/ # CLI 인터페이스 │ │ ├── index.ts # CLI 메인 진입점 │ │ ├── commands/ │ │ │ ├── login.ts │ │ │ ├── logout.ts │ │ │ ├── query.ts │ │ │ ├── projects.ts │ │ │ ├── validate.ts │ │ │ ├── status.ts │ │ │ └── config.ts │ │ └── ui/ │ │ ├── formatters.ts # 테이블, JSON, CSV 포맷팅 │ │ ├── prompts.ts # 대화형 입력 │ │ └── spinner.ts # 로딩 표시 │ │ │ └── mcp/ │ ├── server.ts # MCP 서버 메인 │ └── tools/ │ ├── login.ts # MCP Tool: login │ ├── query.ts # MCP Tool: query │ ├── projects.ts # MCP Tool: projects │ └── validate.ts # MCP Tool: validate │ ├── dist/ # 빌드 출력 (TypeScript → JavaScript) │ └── test/ ├── core/ ├── cli/ └── mcp/ ``` ## 🔧 Core 모듈 설계 ### 1. AuthManager **책임**: WhaTap 인증 처리 및 세션 관리 ```typescript // src/core/auth/AuthManager.ts export interface LoginCredentials { email: string; password: string; serviceUrl?: string; } export interface Session { email: string; accountId: number; cookies: { wa: string; jsessionid: string; }; apiToken?: string; serviceUrl: string; createdAt: Date; expiresAt: Date; } export class AuthManager { private session: Session | null = null; private sessionStore: SessionStore; constructor(sessionStore: SessionStore) { this.sessionStore = sessionStore; } /** * WhaTap 로그인 (3단계) * 1. CSRF 토큰 획득 * 2. 웹 로그인 (쿠키) * 3. API 토큰 획득 */ async login(credentials: LoginCredentials): Promise<Session> { const serviceUrl = credentials.serviceUrl || 'https://service.whatap.io'; // Step 1: CSRF 토큰 획득 const csrf = await this.getCsrfToken(serviceUrl); // Step 2: 웹 로그인 const { wa, jsessionid } = await this.webLogin( serviceUrl, credentials.email, credentials.password, csrf ); // Step 3: API 토큰 획득 const { accountId, apiToken } = await this.getMobileToken( serviceUrl, credentials.email, credentials.password, { wa, jsessionid } ); // 세션 생성 및 저장 this.session = { email: credentials.email, accountId, cookies: { wa, jsessionid }, apiToken, serviceUrl, createdAt: new Date(), expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24시간 }; await this.sessionStore.save(this.session); return this.session; } /** * 저장된 세션 복원 */ async loadSession(): Promise<Session | null> { this.session = await this.sessionStore.load(); return this.session; } /** * 로그아웃 */ async logout(): Promise<void> { this.session = null; await this.sessionStore.clear(); } /** * 세션 유효성 확인 */ isAuthenticated(): boolean { if (!this.session) return false; return new Date(this.session.expiresAt) > new Date(); } /** * 쿠키 헤더 문자열 생성 */ getCookieHeader(): string { if (!this.session) throw new Error('Not authenticated'); return `wa=${this.session.cookies.wa}; JSESSIONID=${this.session.cookies.jsessionid}`; } getSession(): Session { if (!this.session) throw new Error('Not authenticated'); return this.session; } // Private methods private async getCsrfToken(serviceUrl: string): Promise<string> { // GET /account/login?lang=en // HTML 파싱하여 CSRF 토큰 추출 } private async webLogin( serviceUrl: string, email: string, password: string, csrf: string ): Promise<{ wa: string; jsessionid: string }> { // POST /account/login // Form data: email, password, _csrf, rememberMe=on // Response cookies에서 wa, JSESSIONID 추출 } private async getMobileToken( serviceUrl: string, email: string, password: string, cookies: { wa: string; jsessionid: string } ): Promise<{ accountId: number; apiToken: string }> { // POST /mobile/api/login // With cookies from webLogin } } ``` ### 2. SessionStore **책임**: 세션 암호화 저장 및 로드 ```typescript // src/core/auth/SessionStore.ts import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; import * as crypto from 'crypto'; export class SessionStore { private configDir: string; private sessionFile: string; private encryptionKey: Buffer; constructor() { this.configDir = path.join(os.homedir(), '.whatap-mxql'); this.sessionFile = path.join(this.configDir, 'session.enc'); // 암호화 키 생성/로드 this.encryptionKey = this.getOrCreateEncryptionKey(); } async save(session: Session): Promise<void> { await fs.mkdir(this.configDir, { recursive: true }); const encrypted = this.encrypt(JSON.stringify(session)); await fs.writeFile(this.sessionFile, encrypted, 'utf-8'); } async load(): Promise<Session | null> { try { const encrypted = await fs.readFile(this.sessionFile, 'utf-8'); const decrypted = this.decrypt(encrypted); const session = JSON.parse(decrypted) as Session; // 만료 확인 if (new Date(session.expiresAt) < new Date()) { await this.clear(); return null; } return session; } catch (error) { if ((error as any).code === 'ENOENT') { return null; } throw error; } } async clear(): Promise<void> { try { await fs.unlink(this.sessionFile); } catch (error) { if ((error as any).code !== 'ENOENT') { throw error; } } } private encrypt(text: string): string { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv('aes-256-gcm', this.encryptionKey, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag(); return JSON.stringify({ iv: iv.toString('hex'), encrypted, authTag: authTag.toString('hex'), }); } private decrypt(encrypted: string): string { const { iv, encrypted: encryptedText, authTag } = JSON.parse(encrypted); const decipher = crypto.createDecipheriv( 'aes-256-gcm', this.encryptionKey, Buffer.from(iv, 'hex') ); decipher.setAuthTag(Buffer.from(authTag, 'hex')); let decrypted = decipher.update(encryptedText, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } private getOrCreateEncryptionKey(): Buffer { const keyFile = path.join(this.configDir, '.key'); try { return fs.readFileSync(keyFile); } catch { const key = crypto.randomBytes(32); fs.mkdirSync(this.configDir, { recursive: true }); fs.writeFileSync(keyFile, key); fs.chmodSync(keyFile, 0o600); // 소유자만 읽기/쓰기 return key; } } } ``` ### 3. WhatapClient **책임**: WhaTap API 호출 ```typescript // src/core/api/WhatapClient.ts import axios, { AxiosInstance } from 'axios'; import type { AuthManager } from '../auth/AuthManager'; export interface MxqlRequest { type: 'mxql'; pcode: number; params: { mql: string; stime: number; etime: number; limit: number; }; path: 'text' | 'path'; } export interface Project { pcode: number; name: string; type: string; status: string; } export class WhatapClient { private axios: AxiosInstance; private authManager: AuthManager; constructor(authManager: AuthManager) { this.authManager = authManager; const session = authManager.getSession(); this.axios = axios.create({ baseURL: session.serviceUrl, timeout: 45000, headers: { 'User-Agent': 'WhatapMxqlCLI/1.0.0', }, }); // 쿠키 인터셉터 this.axios.interceptors.request.use((config) => { config.headers['Cookie'] = this.authManager.getCookieHeader(); return config; }); } /** * MXQL 쿼리 실행 */ async executeMxql(request: MxqlRequest): Promise<any> { const response = await this.axios.post('/yard/api/flush', request); return response.data; } /** * 프로젝트 목록 조회 */ async getProjects(): Promise<Project[]> { // TODO: 실제 API 엔드포인트 확인 필요 const response = await this.axios.get('/v2/project/list'); return response.data; } /** * 프로젝트 상세 조회 */ async getProject(pcode: number): Promise<Project> { const response = await this.axios.get(`/v2/project/${pcode}`); return response.data; } } ``` ### 4. MxqlExecutor **책임**: MXQL 쿼리 실행 편의 기능 ```typescript // src/core/api/MxqlExecutor.ts import type { WhatapClient, MxqlRequest } from './WhatapClient'; export interface QueryOptions { pcode: number; mql: string; stime?: number; etime?: number; limit?: number; } export interface QueryResult { data: any[]; rowCount: number; executionTimeMs: number; query: QueryOptions; } export class MxqlExecutor { constructor(private client: WhatapClient) {} async execute(options: QueryOptions): Promise<QueryResult> { const stime = options.stime || Date.now() - 5 * 60 * 1000; // 기본: 5분 전 const etime = options.etime || Date.now(); const limit = options.limit || 10000; const request: MxqlRequest = { type: 'mxql', pcode: options.pcode, params: { mql: options.mql, stime, etime, limit, }, path: 'text', }; const startTime = Date.now(); const data = await this.client.executeMxql(request); const executionTimeMs = Date.now() - startTime; return { data: Array.isArray(data) ? data : [data], rowCount: Array.isArray(data) ? data.length : 1, executionTimeMs, query: options, }; } async executeFromFile(pcode: number, filePath: string): Promise<QueryResult> { const fs = await import('fs/promises'); const mql = await fs.readFile(filePath, 'utf-8'); return this.execute({ pcode, mql }); } } ``` --- ## 🖥️ CLI 모듈 설계 ### CLI 프레임워크: Commander.js **선택 이유**: - 간단하고 직관적 - TypeScript 지원 우수 - 널리 사용되는 표준 ### CLI 진입점 ```typescript // src/cli/index.ts import { Command } from 'commander'; import { loginCommand } from './commands/login'; import { logoutCommand } from './commands/logout'; import { queryCommand } from './commands/query'; import { projectsCommand } from './commands/projects'; import { validateCommand } from './commands/validate'; import { statusCommand } from './commands/status'; const program = new Command(); program .name('whatap') .description('WhaTap MXQL CLI - Query and manage WhaTap monitoring data') .version('1.0.0'); // 명령어 등록 program.addCommand(loginCommand); program.addCommand(logoutCommand); program.addCommand(queryCommand); program.addCommand(projectsCommand); program.addCommand(validateCommand); program.addCommand(statusCommand); program.parse(); ``` ### 명령어: whatap login ```typescript // src/cli/commands/login.ts import { Command } from 'commander'; import { input, password } from '@inquirer/prompts'; import ora from 'ora'; import chalk from 'chalk'; import { AuthManager } from '../../core/auth/AuthManager'; import { SessionStore } from '../../core/auth/SessionStore'; export const loginCommand = new Command('login') .description('Login to WhaTap service') .option('-e, --email <email>', 'WhaTap account email') .option('-p, --password <password>', 'WhaTap account password') .option('-u, --url <url>', 'Service URL', 'https://service.whatap.io') .action(async (options) => { try { // 대화형 입력 (옵션으로 제공되지 않은 경우) const email = options.email || await input({ message: 'WhaTap Email:', validate: (value) => value.includes('@') || 'Invalid email format' }); const pwd = options.password || await password({ message: 'Password:', mask: '*' }); // 로그인 진행 const spinner = ora('Logging in to WhaTap...').start(); const sessionStore = new SessionStore(); const authManager = new AuthManager(sessionStore); const session = await authManager.login({ email, password: pwd, serviceUrl: options.url }); spinner.succeed(chalk.green('Login successful!')); console.log(chalk.blue('\nSession Information:')); console.log(` Account ID: ${session.accountId}`); console.log(` Email: ${session.email}`); console.log(` Service: ${session.serviceUrl}`); console.log(` Expires: ${new Date(session.expiresAt).toLocaleString()}`); } catch (error) { console.error(chalk.red('Login failed:'), error.message); process.exit(1); } }); ``` ### 명령어: whatap query ```typescript // src/cli/commands/query.ts import { Command } from 'commander'; import { input, editor } from '@inquirer/prompts'; import ora from 'ora'; import chalk from 'chalk'; import Table from 'cli-table3'; import { AuthManager } from '../../core/auth/AuthManager'; import { SessionStore } from '../../core/auth/SessionStore'; import { WhatapClient } from '../../core/api/WhatapClient'; import { MxqlExecutor } from '../../core/api/MxqlExecutor'; import { formatTable, formatJson, formatCsv } from '../ui/formatters'; export const queryCommand = new Command('query') .description('Execute MXQL query') .requiredOption('-p, --pcode <number>', 'Project code', parseInt) .option('-m, --mql <query>', 'MXQL query string') .option('-f, --file <path>', 'Read MXQL query from file') .option('-i, --interactive', 'Interactive query mode') .option('--stime <timestamp>', 'Start time (ms)', parseInt) .option('--etime <timestamp>', 'End time (ms)', parseInt) .option('--limit <number>', 'Result limit', parseInt, 10000) .option('--format <type>', 'Output format (table|json|csv)', 'table') .action(async (options) => { try { // 인증 확인 const sessionStore = new SessionStore(); const authManager = new AuthManager(sessionStore); await authManager.loadSession(); if (!authManager.isAuthenticated()) { console.error(chalk.red('Not authenticated. Please run: whatap login')); process.exit(1); } // MXQL 쿼리 가져오기 let mql: string; if (options.interactive) { mql = await editor({ message: 'Enter MXQL query:', default: 'CATEGORY app_counter\nTAGLOAD\nSELECT' }); } else if (options.file) { const fs = await import('fs/promises'); mql = await fs.readFile(options.file, 'utf-8'); } else if (options.mql) { mql = options.mql; } else { console.error(chalk.red('Please provide query with --mql, --file, or --interactive')); process.exit(1); } // 쿼리 실행 const spinner = ora('Executing MXQL query...').start(); const client = new WhatapClient(authManager); const executor = new MxqlExecutor(client); const result = await executor.execute({ pcode: options.pcode, mql, stime: options.stime, etime: options.etime, limit: options.limit, }); spinner.succeed(chalk.green(`Query executed successfully in ${result.executionTimeMs}ms`)); // 결과 출력 console.log(chalk.blue(`\nRows returned: ${result.rowCount}`)); console.log(''); if (options.format === 'json') { console.log(formatJson(result.data)); } else if (options.format === 'csv') { console.log(formatCsv(result.data)); } else { console.log(formatTable(result.data)); } } catch (error) { console.error(chalk.red('Query failed:'), error.message); process.exit(1); } }); ``` ### 명령어: whatap projects ```typescript // src/cli/commands/projects.ts import { Command } from 'commander'; import ora from 'ora'; import chalk from 'chalk'; import Table from 'cli-table3'; import { AuthManager } from '../../core/auth/AuthManager'; import { SessionStore } from '../../core/auth/SessionStore'; import { WhatapClient } from '../../core/api/WhatapClient'; export const projectsCommand = new Command('projects') .description('Manage WhaTap projects') .addCommand( new Command('list') .description('List all accessible projects') .option('--json', 'Output as JSON') .action(async (options) => { try { // 인증 확인 const sessionStore = new SessionStore(); const authManager = new AuthManager(sessionStore); await authManager.loadSession(); if (!authManager.isAuthenticated()) { console.error(chalk.red('Not authenticated. Please run: whatap login')); process.exit(1); } const spinner = ora('Fetching projects...').start(); const client = new WhatapClient(authManager); const projects = await client.getProjects(); spinner.succeed(chalk.green(`Found ${projects.length} projects`)); if (options.json) { console.log(JSON.stringify(projects, null, 2)); } else { const table = new Table({ head: ['PCode', 'Name', 'Type', 'Status'], colWidths: [12, 40, 15, 12] }); projects.forEach(p => { table.push([p.pcode, p.name, p.type, p.status]); }); console.log(table.toString()); } } catch (error) { console.error(chalk.red('Failed to fetch projects:'), error.message); process.exit(1); } }) ) .addCommand( new Command('show') .description('Show project details') .argument('<pcode>', 'Project code') .action(async (pcode) => { // 프로젝트 상세 정보 조회 }) ); ``` ### UI 포맷터 ```typescript // src/cli/ui/formatters.ts import Table from 'cli-table3'; export function formatTable(data: any[]): string { if (data.length === 0) { return 'No data'; } const keys = Object.keys(data[0]); const table = new Table({ head: keys, colWidths: keys.map(() => 20) }); data.forEach(row => { table.push(keys.map(key => String(row[key]))); }); return table.toString(); } export function formatJson(data: any[]): string { return JSON.stringify(data, null, 2); } export function formatCsv(data: any[]): string { if (data.length === 0) { return ''; } const keys = Object.keys(data[0]); const header = keys.join(','); const rows = data.map(row => keys.map(key => { const value = String(row[key]); return value.includes(',') ? `"${value}"` : value; }).join(',') ); return [header, ...rows].join('\n'); } ``` --- ## 🔌 MCP 모듈 설계 ### MCP 서버 메인 ```typescript // src/mcp/server.ts import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; // Core 모듈 import (CLI와 공유) import { AuthManager } from '../core/auth/AuthManager'; import { SessionStore } from '../core/auth/SessionStore'; import { WhatapClient } from '../core/api/WhatapClient'; import { MxqlExecutor } from '../core/api/MxqlExecutor'; // MCP Tools import { loginTool, handleLogin } from './tools/login'; import { queryTool, handleQuery } from './tools/query'; import { projectsTool, handleProjects } from './tools/projects'; import { validateTool, handleValidate } from './tools/validate'; // MCP 서버 생성 const server = new Server( { name: 'whatap-mxql-server', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); // 공유 인스턴스 const sessionStore = new SessionStore(); const authManager = new AuthManager(sessionStore); let whatapClient: WhatapClient | null = null; // Tool 목록 제공 server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [loginTool, queryTool, projectsTool, validateTool], }; }); // Tool 실행 server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'whatap_login': const session = await handleLogin(authManager, args); whatapClient = new WhatapClient(authManager); return { content: [ { type: 'text', text: JSON.stringify({ success: true, accountId: session.accountId, email: session.email, }), }, ], }; case 'whatap_query': if (!whatapClient) { throw new Error('Not authenticated. Please call whatap_login first.'); } const executor = new MxqlExecutor(whatapClient); const result = await handleQuery(executor, args); return { content: [ { type: 'text', text: JSON.stringify(result), }, ], }; case 'whatap_projects': if (!whatapClient) { throw new Error('Not authenticated. Please call whatap_login first.'); } const projects = await handleProjects(whatapClient, args); return { content: [ { type: 'text', text: JSON.stringify(projects), }, ], }; case 'whatap_validate': const validation = await handleValidate(args); return { content: [ { type: 'text', text: JSON.stringify(validation), }, ], }; default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: 'text', text: JSON.stringify({ success: false, error: error.message, }), }, ], isError: true, }; } }); // 서버 시작 async function main() { const transport = new StdioServerTransport(); await server.connect(transport); // 세션 복원 시도 await authManager.loadSession(); if (authManager.isAuthenticated()) { whatapClient = new WhatapClient(authManager); } } main().catch(console.error); ``` ### MCP Tool 정의 ```typescript // src/mcp/tools/query.ts import type { MxqlExecutor } from '../../core/api/MxqlExecutor'; export const queryTool = { name: 'whatap_query', description: 'Execute MXQL query against WhaTap monitoring data', inputSchema: { type: 'object', properties: { pcode: { type: 'number', description: 'Project code (pcode)', }, mql: { type: 'string', description: 'MXQL query string', }, stime: { type: 'number', description: 'Start time in milliseconds (optional)', }, etime: { type: 'number', description: 'End time in milliseconds (optional)', }, limit: { type: 'number', description: 'Result limit (default: 10000)', }, }, required: ['pcode', 'mql'], }, }; export async function handleQuery(executor: MxqlExecutor, args: any) { const result = await executor.execute({ pcode: args.pcode, mql: args.mql, stime: args.stime, etime: args.etime, limit: args.limit, }); return { success: true, data: result.data, rowCount: result.rowCount, executionTimeMs: result.executionTimeMs, }; } ``` --- ## 📦 package.json ```json { "name": "whatap-mxql-cli", "version": "1.0.0", "description": "WhaTap MXQL CLI and MCP Server", "main": "dist/index.js", "bin": { "whatap": "./bin/whatap.js", "whatap-mcp": "./bin/whatap-mcp.js" }, "scripts": { "build": "tsc", "dev": "tsx src/cli/index.ts", "mcp": "tsx src/mcp/server.ts", "test": "jest", "lint": "eslint src/**/*.ts", "format": "prettier --write src/**/*.ts" }, "keywords": ["whatap", "mxql", "cli", "mcp", "monitoring"], "author": "Your Name", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^0.5.0", "commander": "^12.0.0", "@inquirer/prompts": "^4.0.0", "axios": "^1.6.0", "ora": "^8.0.0", "chalk": "^5.3.0", "cli-table3": "^0.6.3", "cheerio": "^1.0.0-rc.12" }, "devDependencies": { "@types/node": "^20.0.0", "typescript": "^5.3.0", "tsx": "^4.7.0", "jest": "^29.7.0", "@types/jest": "^29.5.0", "eslint": "^8.56.0", "prettier": "^3.2.0" } } ``` --- ## 🚀 사용 시나리오 ### 시나리오 1: CLI 단독 사용 ```bash # 설치 npm install -g whatap-mxql-cli # 로그인 whatap login # Email: user@example.com # Password: ******** # ✓ Login successful! # 프로젝트 목록 확인 whatap projects list # ┌────────────┬──────────────────┬─────────┬────────┐ # │ PCode │ Name │ Type │ Status │ # ├────────────┼──────────────────┼─────────┼────────┤ # │ 12345 │ Production APM │ APM │ active │ # │ 67890 │ DB Monitoring │ DBX │ active │ # └────────────┴──────────────────┴─────────┴────────┘ # MXQL 쿼리 실행 whatap query --pcode 12345 --mql "CATEGORY app_counter\nTAGLOAD\nSELECT" # 파일에서 쿼리 실행 whatap query --pcode 12345 --file my_query.mql --format json > result.json # 대화형 쿼리 whatap query --pcode 12345 --interactive ``` ### 시나리오 2: Claude Code와 MCP 통합 **MCP 설정** (`~/.config/claude-code/mcp.json`): ```json { "mcpServers": { "whatap": { "command": "whatap-mcp" } } } ``` **Claude Code에서 사용**: ``` User: WhaTap에 로그인해줘 (user@example.com / password123) Claude: [whatap_login tool 호출] ✓ WhaTap에 성공적으로 로그인했습니다. Account ID: 12345 User: pcode 12345의 최근 5분간 CPU 사용률을 조회해줘 Claude: [whatap_query tool 호출] MXQL 쿼리를 실행했습니다. 결과: - 총 42개 레코드 - 실행 시간: 234ms - 평균 CPU: 45.2% ... User: 내가 접근 가능한 프로젝트 목록 보여줘 Claude: [whatap_projects tool 호출] 접근 가능한 프로젝트 목록: 1. Production APM (12345) - APM - active 2. DB Monitoring (67890) - DBX - active ``` --- ## 🔒 보안 고려사항 1. **세션 저장소** - AES-256-GCM 암호화 - 키 파일 권한: 0600 (소유자만 읽기/쓰기) - 24시간 자동 만료 2. **비밀번호 처리** - 메모리에만 존재 (저장 안 함) - CLI: `@inquirer/prompts`의 password 타입 (마스킹) 3. **HTTPS 강제** - serviceUrl이 https가 아니면 거부 4. **에러 처리** - 민감한 정보 로그 출력 금지 - 스택 트레이스 제한 --- ## 🧪 테스트 전략 ### 단위 테스트 ```typescript // test/core/auth/AuthManager.test.ts describe('AuthManager', () => { it('should login successfully with valid credentials', async () => { // ... }); it('should throw error for invalid credentials', async () => { // ... }); it('should restore session from storage', async () => { // ... }); }); ``` ### 통합 테스트 ```typescript // test/cli/commands/query.test.ts describe('whatap query command', () => { it('should execute query and display results', async () => { // CLI 명령어 실행 및 출력 검증 }); }); ``` ### E2E 테스트 ```bash # test/e2e/test.sh whatap login --email test@example.com --password test123 whatap projects list whatap query --pcode 12345 --mql "CATEGORY app_counter" whatap logout ``` --- ## 📋 개발 로드맵 ### Phase 1: Core 구현 (1주) - [ ] AuthManager 구현 - [ ] SessionStore 구현 - [ ] WhatapClient 구현 - [ ] MxqlExecutor 구현 - [ ] 단위 테스트 ### Phase 2: CLI 구현 (1주) - [ ] CLI 프레임워크 설정 - [ ] login, logout 명령어 - [ ] query 명령어 (기본) - [ ] projects 명령어 - [ ] UI 포맷터 (table, json, csv) ### Phase 3: CLI 고도화 (3-4일) - [ ] query 명령어 고도화 (interactive, file) - [ ] validate 명령어 - [ ] status, config 명령어 - [ ] 에러 핸들링 강화 ### Phase 4: MCP 구현 (2-3일) - [ ] MCP 서버 기본 구조 - [ ] MCP Tools 구현 - [ ] MCP <-> Core 통합 ### Phase 5: 테스트 및 문서화 (3-4일) - [ ] 통합 테스트 - [ ] E2E 테스트 - [ ] README 작성 - [ ] 사용 가이드 문서 ### Phase 6: 배포 (1-2일) - [ ] npm 패키지 배포 - [ ] GitHub Actions CI/CD - [ ] 버전 관리 전략 **총 예상 기간: 2.5-3주**

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/devload/whatap-mxql-cli'

If you have feedback or need assistance with the MCP directory API, please join our Discord server