Skip to main content
Glama
APITester.vue34.5 kB
<template> <div class="api-tester"> <!-- 页面头部 --> <div class="header-section"> <div class="header-content"> <h1> <el-icon><Tools /></el-icon> API测试工具 </h1> <p class="header-description"> 测试转换后的MCP工具,验证API调用和参数配置 </p> </div> <div class="header-actions"> <el-button type="primary" :icon="Plus" @click="showCreateTestCaseDialog" :disabled="!selectedTool" > 创建测试用例 </el-button> <el-button :icon="Refresh" @click="refreshData" :loading="loading"> 刷新 </el-button> </div> </div> <!-- 主要内容区域 --> <div class="main-content"> <!-- 左侧工具列表 --> <div class="tools-panel"> <div class="panel-header"> <h3>可用工具</h3> <el-input v-model="toolSearchText" placeholder="搜索工具..." :prefix-icon="Search" clearable size="small" /> </div> <div class="tools-list" v-loading="loadingTools"> <div v-for="tool in filteredTools" :key="tool.id" class="tool-item" :class="{ active: selectedTool?.id === tool.id }" @click="selectTool(tool)" > <div class="tool-info"> <div class="tool-name">{{ tool.name }}</div> <div class="tool-method">{{ tool.method.toUpperCase() }}</div> </div> <div class="tool-description">{{ tool.description }}</div> </div> <div v-if="filteredTools.length === 0" class="empty-state"> <el-empty description="暂无可用工具"> <el-button type="primary" @click="$router.push('/openapi')"> 去创建工具 </el-button> </el-empty> </div> </div> </div> <!-- 右侧测试区域 --> <div class="testing-panel" v-if="selectedTool"> <el-tabs v-model="activeTab" class="testing-tabs"> <!-- 手动测试标签页 --> <el-tab-pane label="手动测试" name="manual"> <div class="manual-test-form"> <div class="form-header"> <h4>{{ selectedTool.name }}</h4> <el-tag :type="getMethodTagType(selectedTool.method)"> {{ selectedTool.method.toUpperCase() }} </el-tag> </div> <div class="tool-description-text"> {{ selectedTool.description }} </div> <!-- 参数表单 --> <div class="parameters-section"> <h5>参数配置</h5> <el-form ref="parametersFormRef" :model="testParameters" label-width="120px" class="parameters-form" > <div v-if="!hasParameters" class="no-parameters"> 此工具无需参数 </div> <template v-else> <el-form-item v-for="[paramName, paramSchema] in Object.entries( toolParameters, )" :key="paramName" :label="paramName" :prop="paramName" :rules="getParameterRules(paramName, paramSchema)" > <div class="parameter-input"> <component :is="getParameterComponent(paramSchema.type)" v-model="testParameters[paramName]" :placeholder="getParameterPlaceholder(paramSchema)" :disabled="loading" v-bind="getParameterProps(paramSchema)" /> <div class="parameter-info"> <span class="parameter-type">{{ paramSchema.type }}</span> <span v-if="isRequired(paramName)" class="required-mark" >*</span > </div> </div> <div v-if="paramSchema.description" class="parameter-description" > {{ paramSchema.description }} </div> </el-form-item> </template> </el-form> </div> <!-- 测试按钮 --> <div class="test-actions"> <el-button type="primary" size="large" :loading="testing" @click="executeTest" :icon="CaretRight" > {{ testing ? "测试中..." : "执行测试" }} </el-button> <el-button @click="resetParameters" :disabled="testing"> 重置参数 </el-button> <el-button @click="fillSampleData" :disabled="testing"> 填充示例数据 </el-button> </div> <!-- 测试结果 --> <div v-if="testResult" class="result-section"> <div class="result-header"> <h5>测试结果</h5> <el-tag :type="testResult.success ? 'success' : 'danger'"> {{ testResult.success ? "成功" : "失败" }} </el-tag> <span class="execution-time"> 执行时间: {{ testResult.executionTime }}ms </span> </div> <div class="result-content"> <el-alert v-if="!testResult.success" :title="testResult.error" type="error" show-icon /> <div v-else class="success-result"> <el-input type="textarea" :rows="8" :value="JSON.stringify(testResult.data, null, 2)" readonly /> </div> </div> <div class="result-actions"> <el-button size="small" @click="copyResult"> <el-icon><CopyDocument /></el-icon> 复制结果 </el-button> <el-button size="small" @click="saveAsTestCase" v-if="testResult.success" > <el-icon><Plus /></el-icon> 保存为测试用例 </el-button> </div> </div> </div> </el-tab-pane> <!-- 测试用例标签页 --> <el-tab-pane label="测试用例" name="testcases"> <div class="test-cases-section"> <div class="test-cases-header"> <div class="search-filters"> <el-input v-model="testCaseSearchText" placeholder="搜索测试用例..." :prefix-icon="Search" clearable style="width: 200px" /> <el-select v-model="selectedTag" placeholder="筛选标签" clearable style="width: 150px" > <el-option v-for="tag in availableTags" :key="tag" :label="tag" :value="tag" /> </el-select> </div> </div> <div class="test-cases-list"> <div v-for="testCase in filteredTestCases" :key="testCase.id" class="test-case-item" > <div class="test-case-info"> <div class="test-case-name">{{ testCase.name }}</div> <div class="test-case-meta"> <el-tag v-for="tag in testCase.tags" :key="tag" size="small" style="margin-right: 4px" > {{ tag }} </el-tag> <span class="test-case-date"> {{ formatDate(testCase.createdAt) }} </span> </div> </div> <div class="test-case-actions"> <el-button size="small" @click="runTestCase(testCase)"> 运行 </el-button> <el-button size="small" @click="editTestCase(testCase)"> 编辑 </el-button> <el-button size="small" type="danger" @click="deleteTestCase(testCase.id)" > 删除 </el-button> </div> </div> <div v-if="filteredTestCases.length === 0" class="empty-state"> <el-empty description="暂无测试用例" /> </div> </div> </div> </el-tab-pane> <!-- 测试历史标签页 --> <el-tab-pane label="测试历史" name="history"> <div class="test-history-section"> <div class="history-list"> <div v-for="historyItem in recentHistory" :key="historyItem.id" class="history-item" > <div class="history-info"> <div class="history-tool"> {{ getToolName(historyItem.toolId) }} </div> <div class="history-time"> {{ formatDateTime(historyItem.timestamp) }} </div> </div> <div class="history-result"> <el-tag :type="historyItem.result.success ? 'success' : 'danger'" > {{ historyItem.result.success ? "成功" : "失败" }} </el-tag> <span class="execution-time"> {{ historyItem.result.executionTime }}ms </span> </div> <div class="history-actions"> <el-button size="small" @click="viewHistoryDetails(historyItem)" > 查看详情 </el-button> <el-button size="small" @click="rerunFromHistory(historyItem)" > 重新运行 </el-button> </div> </div> <div v-if="recentHistory.length === 0" class="empty-state"> <el-empty description="暂无测试历史" /> </div> </div> </div> </el-tab-pane> </el-tabs> </div> <!-- 未选择工具时的提示 --> <div v-else class="no-tool-selected"> <el-empty description="请选择一个工具开始测试" /> </div> </div> <!-- 创建测试用例对话框 --> <el-dialog v-model="createTestCaseDialogVisible" title="创建测试用例" width="500px" :close-on-click-modal="false" > <el-form ref="createTestCaseFormRef" :model="newTestCase" label-width="80px" :rules="testCaseRules" > <el-form-item label="名称" prop="name"> <el-input v-model="newTestCase.name" placeholder="输入测试用例名称" /> </el-form-item> <el-form-item label="标签" prop="tags"> <el-input v-model="tagsInput" placeholder="输入标签,用逗号分隔" /> </el-form-item> <el-form-item label="期望结果"> <el-input type="textarea" v-model="newTestCase.expectedResult" placeholder="描述期望的测试结果(可选)" :rows="3" /> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button @click="createTestCaseDialogVisible = false" >取消</el-button > <el-button type="primary" @click="createTestCase">创建</el-button> </div> </template> </el-dialog> <!-- 历史详情对话框 --> <el-dialog v-model="historyDetailsDialogVisible" title="测试历史详情" width="600px" > <div v-if="selectedHistoryItem" class="history-details"> <div class="detail-section"> <h4>基本信息</h4> <div class="detail-content"> <p> <strong>工具:</strong> {{ getToolName(selectedHistoryItem.toolId) }} </p> <p> <strong>时间:</strong> {{ formatDateTime(selectedHistoryItem.timestamp) }} </p> <p> <strong>状态:</strong> <el-tag :type=" selectedHistoryItem.result.success ? 'success' : 'danger' " > {{ selectedHistoryItem.result.success ? "成功" : "失败" }} </el-tag> </p> <p> <strong>执行时间:</strong> {{ selectedHistoryItem.result.executionTime }}ms </p> </div> </div> <div class="detail-section"> <h4>参数</h4> <el-input type="textarea" :value="JSON.stringify(selectedHistoryItem.parameters, null, 2)" readonly :rows="4" /> </div> <div class="detail-section"> <h4>结果</h4> <el-input type="textarea" :value=" selectedHistoryItem.result.success ? JSON.stringify(selectedHistoryItem.result.data, null, 2) : selectedHistoryItem.result.error " readonly :rows="6" /> </div> </div> </el-dialog> </div> </template> <script setup lang="ts"> import { ref, computed, onMounted, nextTick } from "vue"; import { ElMessage, ElMessageBox, type FormInstance, type FormRules, } from "element-plus"; import { Tools, Plus, Refresh, Search, CaretRight, CopyDocument, } from "@element-plus/icons-vue"; import { useTestingStore } from "@/stores/testing"; import { useOpenAPIStore } from "@/stores/openapi"; import { convertOpenAPIToMCPTools } from "@/utils/openapi"; import type { MCPTool, TestCase, ToolResult } from "@/types"; // 导入全局功能 import { useConfirmation } from "@/composables/useConfirmation"; import { useFormValidation } from "@/composables/useFormValidation"; import { usePerformanceMonitor } from "@/composables/usePerformance"; import LoadingOverlay from "@/shared/components/ui/LoadingOverlay.vue"; // Stores const testingStore = useTestingStore(); const openApiStore = useOpenAPIStore(); // 全局功能 const { confirmDelete: globalConfirmDelete, confirmSave } = useConfirmation(); const { startMonitoring, stopMonitoring, measureFunction } = usePerformanceMonitor(); // Reactive data const loading = ref(false); const loadingTools = ref(false); const testing = ref(false); const activeTab = ref("manual"); // 工具选择和搜索 const selectedTool = ref<MCPTool | null>(null); const toolSearchText = ref(""); // 测试参数 const testParameters = ref<Record<string, any>>({}); const testResult = ref<ToolResult | null>(null); const parametersFormRef = ref<FormInstance>(); // 测试用例管理 const createTestCaseDialogVisible = ref(false); const newTestCase = ref({ name: "", expectedResult: "", tags: [] as string[], }); const tagsInput = ref(""); const createTestCaseFormRef = ref<FormInstance>(); const testCaseSearchText = ref(""); const selectedTag = ref(""); // 历史记录 const historyDetailsDialogVisible = ref(false); const selectedHistoryItem = ref<any>(null); // Computed properties const tools = computed(() => { const allTools: MCPTool[] = []; for (const spec of openApiStore.validSpecs) { try { const specTools = convertOpenAPIToMCPTools(spec.content); allTools.push(...specTools); } catch (error) { console.warn(`Failed to convert spec ${spec.name} to MCP tools:`, error); } } return allTools; }); const filteredTools = computed(() => { if (!toolSearchText.value) return tools.value; const searchLower = toolSearchText.value.toLowerCase(); return tools.value.filter( (tool: MCPTool) => tool.name.toLowerCase().includes(searchLower) || tool.description.toLowerCase().includes(searchLower) || tool.method.toLowerCase().includes(searchLower), ); }); const toolParameters = computed(() => { if (!selectedTool.value?.parameters?.properties) return {}; return selectedTool.value.parameters.properties; }); const hasParameters = computed(() => { return Object.keys(toolParameters.value).length > 0; }); const testCases = computed(() => { if (!selectedTool.value) return []; return testingStore.getTestCasesByTool(selectedTool.value.id); }); const filteredTestCases = computed(() => { let filtered = testCases.value; if (testCaseSearchText.value) { const searchLower = testCaseSearchText.value.toLowerCase(); filtered = filtered.filter((tc) => tc.name.toLowerCase().includes(searchLower), ); } if (selectedTag.value) { filtered = filtered.filter((tc) => tc.tags.includes(selectedTag.value)); } return filtered; }); const availableTags = computed(() => { const tags = new Set<string>(); testCases.value.forEach((tc) => { tc.tags.forEach((tag) => tags.add(tag)); }); return Array.from(tags); }); const recentHistory = computed(() => { return testingStore.getRecentTestHistory(20); }); // 表单验证规则 const testCaseRules: FormRules = { name: [ { required: true, message: "请输入测试用例名称", trigger: "blur" }, { min: 2, max: 50, message: "长度在 2 到 50 个字符", trigger: "blur" }, ], }; // Methods const selectTool = (tool: MCPTool) => { selectedTool.value = tool; resetParameters(); testResult.value = null; }; const resetParameters = () => { testParameters.value = {}; // 设置默认值 Object.entries(toolParameters.value).forEach(([name, schema]) => { testParameters.value[name] = getDefaultValue(schema); }); }; const fillSampleData = () => { Object.entries(toolParameters.value).forEach(([name, schema]) => { testParameters.value[name] = getSampleValue(schema); }); ElMessage.success("已填充示例数据"); }; const getDefaultValue = (schema: any): any => { if (schema.default !== undefined) return schema.default; switch (schema.type) { case "string": return ""; case "number": case "integer": return schema.minimum || 0; case "boolean": return false; case "array": return []; case "object": return {}; default: return ""; } }; const getSampleValue = (schema: any): any => { if (schema.default !== undefined) return schema.default; switch (schema.type) { case "string": if (schema.enum) return schema.enum[0]; if (schema.format === "email") return "test@example.com"; if (schema.format === "date") return new Date().toISOString().split("T")[0]; if (schema.format === "date-time") return new Date().toISOString(); return "sample text"; case "number": return schema.minimum || 123.45; case "integer": return schema.minimum || 123; case "boolean": return true; case "array": return ["sample item"]; case "object": return { key: "value" }; default: return "sample"; } }; const executeTest = async () => { if (!selectedTool.value) return; // 验证表单 if (parametersFormRef.value) { const valid = await parametersFormRef.value.validate().catch(() => false); if (!valid) { ElMessage.error("请检查参数输入"); return; } } testing.value = true; testResult.value = null; try { const result = await measureFunction("executeTest", async () => { return await testingStore.executeToolTest( selectedTool.value!, testParameters.value, ); }); testResult.value = result; if (result.success) { ElMessage.success("测试执行成功"); } else { ElMessage.error(`测试执行失败: ${result.error}`); } } catch (error) { ElMessage.error(`测试执行异常: ${error}`); } finally { testing.value = false; } }; const getParameterComponent = (type: string) => { switch (type) { case "boolean": return "el-switch"; case "number": case "integer": return "el-input-number"; default: return "el-input"; } }; const getParameterProps = (schema: any) => { const props: any = {}; if (schema.type === "number" || schema.type === "integer") { if (schema.minimum !== undefined) props.min = schema.minimum; if (schema.maximum !== undefined) props.max = schema.maximum; if (schema.type === "integer") props.precision = 0; } if (schema.type === "string") { if (schema.minLength !== undefined) props.minlength = schema.minLength; if (schema.maxLength !== undefined) props.maxlength = schema.maxLength; } return props; }; const getParameterPlaceholder = (schema: any): string => { if (schema.description) return schema.description; if (schema.example) return `例如: ${schema.example}`; switch (schema.type) { case "string": return "请输入字符串"; case "number": return "请输入数字"; case "integer": return "请输入整数"; case "boolean": return "选择是否"; default: return "请输入值"; } }; const getParameterRules = (paramName: string, schema: any) => { const rules: any[] = []; if (isRequired(paramName)) { rules.push({ required: true, message: `${paramName} 是必需参数`, trigger: "blur", }); } if (schema.type === "string" && schema.minLength) { rules.push({ min: schema.minLength, message: `最少 ${schema.minLength} 个字符`, trigger: "blur", }); } if (schema.type === "string" && schema.maxLength) { rules.push({ max: schema.maxLength, message: `最多 ${schema.maxLength} 个字符`, trigger: "blur", }); } return rules; }; const isRequired = (paramName: string): boolean => { return selectedTool.value?.parameters?.required?.includes(paramName) || false; }; const getMethodTagType = (method: string) => { const types: Record<string, string> = { GET: "success", POST: "primary", PUT: "warning", DELETE: "danger", PATCH: "info", }; return types[method.toUpperCase()] || "info"; }; const copyResult = async () => { if (!testResult.value) return; const text = testResult.value.success ? JSON.stringify(testResult.value.data, null, 2) : testResult.value.error || "未知错误"; try { await navigator.clipboard.writeText(text); ElMessage.success("结果已复制到剪贴板"); } catch (error) { ElMessage.error("复制失败"); } }; const saveAsTestCase = () => { if (!selectedTool.value || !testResult.value?.success) return; newTestCase.value = { name: `${selectedTool.value.name} 测试用例`, expectedResult: JSON.stringify(testResult.value.data, null, 2), tags: [selectedTool.value.method.toUpperCase(), "from-manual-test"], }; tagsInput.value = newTestCase.value.tags.join(", "); createTestCaseDialogVisible.value = true; }; const showCreateTestCaseDialog = () => { if (!selectedTool.value) return; const template = testingStore.generateTestCaseTemplate(selectedTool.value); newTestCase.value = { name: template.name || "", expectedResult: "", tags: template.tags || [], }; tagsInput.value = newTestCase.value.tags.join(", "); createTestCaseDialogVisible.value = true; }; const createTestCase = async () => { if (!createTestCaseFormRef.value || !selectedTool.value) return; const valid = await createTestCaseFormRef.value.validate().catch(() => false); if (!valid) return; // 解析标签 const tags = tagsInput.value .split(",") .map((tag) => tag.trim()) .filter((tag) => tag); testingStore.createTestCase({ name: newTestCase.value.name, toolId: selectedTool.value.id, parameters: { ...testParameters.value }, expectedResult: newTestCase.value.expectedResult || undefined, tags, }); createTestCaseDialogVisible.value = false; newTestCase.value = { name: "", expectedResult: "", tags: [] }; tagsInput.value = ""; }; const runTestCase = async (testCase: TestCase) => { if (!selectedTool.value) return; // 设置参数 testParameters.value = { ...testCase.parameters }; // 等待DOM更新后执行测试 await nextTick(); await executeTest(); }; const editTestCase = (testCase: TestCase) => { // 设置为当前测试用例的参数 testParameters.value = { ...testCase.parameters }; activeTab.value = "manual"; ElMessage.info("测试用例参数已加载到手动测试表单"); }; const deleteTestCase = async (testCaseId: string) => { const testCase = testingStore.testCases.find((tc) => tc.id === testCaseId); const testCaseName = testCase?.name || "测试用例"; const confirmed = await globalConfirmDelete(testCaseName); if (!confirmed) return; try { testingStore.deleteTestCase(testCaseId); ElMessage.success("测试用例删除成功"); } catch (error) { ElMessage.error(`删除失败: ${error}`); } }; const viewHistoryDetails = (historyItem: any) => { selectedHistoryItem.value = historyItem; historyDetailsDialogVisible.value = true; }; const rerunFromHistory = async (historyItem: any) => { // 找到对应的工具 const tool = tools.value.find((t: MCPTool) => t.id === historyItem.toolId); if (!tool) { ElMessage.error("工具不存在"); return; } // 切换到该工具并设置参数 selectTool(tool); testParameters.value = { ...historyItem.parameters }; activeTab.value = "manual"; // 执行测试 await nextTick(); await executeTest(); }; const getToolName = (toolId: string): string => { const tool = tools.value.find((t: MCPTool) => t.id === toolId); return tool?.name || "未知工具"; }; const formatDate = (date: Date): string => { return new Intl.DateTimeFormat("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", }).format(new Date(date)); }; const formatDateTime = (date: Date): string => { return new Intl.DateTimeFormat("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", }).format(new Date(date)); }; const refreshData = async () => { loading.value = true; try { await openApiStore.fetchSpecs(); ElMessage.success("数据刷新成功"); } catch (error) { ElMessage.error("数据刷新失败"); } finally { loading.value = false; } }; // 生命周期 onMounted(async () => { loadingTools.value = true; try { await openApiStore.fetchSpecs(); } catch (error) { ElMessage.error("加载工具列表失败"); } finally { loadingTools.value = false; } }); </script> <style scoped> .api-tester { height: 100%; display: flex; flex-direction: column; padding: 20px; background-color: var(--el-bg-color-page); } .header-section { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid var(--el-border-color-light); } .header-content h1 { margin: 0 0 8px 0; color: var(--el-text-color-primary); display: flex; align-items: center; gap: 8px; font-size: 24px; font-weight: 600; } .header-description { margin: 0; color: var(--el-text-color-regular); font-size: 14px; } .header-actions { display: flex; gap: 12px; } .main-content { flex: 1; display: flex; gap: 20px; min-height: 0; } .tools-panel { width: 300px; background: white; border-radius: 8px; padding: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); display: flex; flex-direction: column; } .panel-header { margin-bottom: 16px; } .panel-header h3 { margin: 0 0 12px 0; color: var(--el-text-color-primary); font-size: 16px; font-weight: 600; } .tools-list { flex: 1; overflow-y: auto; } .tool-item { padding: 12px; border: 1px solid var(--el-border-color-light); border-radius: 6px; margin-bottom: 8px; cursor: pointer; transition: all 0.2s; } .tool-item:hover { border-color: var(--el-color-primary); background-color: var(--el-color-primary-light-9); } .tool-item.active { border-color: var(--el-color-primary); background-color: var(--el-color-primary-light-8); } .tool-info { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; } .tool-name { font-weight: 600; color: var(--el-text-color-primary); } .tool-method { font-size: 12px; padding: 2px 6px; border-radius: 4px; background-color: var(--el-color-info-light-8); color: var(--el-color-info); font-weight: 500; } .tool-description { font-size: 12px; color: var(--el-text-color-regular); line-height: 1.4; } .testing-panel { flex: 1; background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); overflow-y: auto; } .testing-tabs { height: 100%; } .manual-test-form { max-width: 800px; } .form-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; } .form-header h4 { margin: 0; color: var(--el-text-color-primary); font-size: 18px; } .tool-description-text { color: var(--el-text-color-regular); margin-bottom: 24px; padding: 12px; background-color: var(--el-bg-color-page); border-radius: 6px; font-size: 14px; line-height: 1.5; } .parameters-section h5 { margin: 0 0 16px 0; color: var(--el-text-color-primary); font-size: 16px; font-weight: 600; } .no-parameters { text-align: center; color: var(--el-text-color-regular); padding: 20px; background-color: var(--el-bg-color-page); border-radius: 6px; font-size: 14px; } .parameter-input { display: flex; align-items: center; gap: 8px; } .parameter-input > :first-child { flex: 1; } .parameter-info { display: flex; align-items: center; gap: 4px; font-size: 12px; color: var(--el-text-color-secondary); } .parameter-type { padding: 2px 6px; background-color: var(--el-color-info-light-8); border-radius: 4px; } .required-mark { color: var(--el-color-danger); font-weight: 600; } .parameter-description { margin-top: 4px; font-size: 12px; color: var(--el-text-color-regular); line-height: 1.4; } .test-actions { margin: 24px 0; display: flex; gap: 12px; } .result-section { margin-top: 24px; padding: 16px; border: 1px solid var(--el-border-color-light); border-radius: 8px; background-color: var(--el-bg-color-page); } .result-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; } .result-header h5 { margin: 0; color: var(--el-text-color-primary); font-size: 16px; font-weight: 600; } .execution-time { color: var(--el-text-color-secondary); font-size: 14px; } .result-content { margin-bottom: 12px; } .result-actions { display: flex; gap: 8px; } .test-cases-section, .test-history-section { max-width: 800px; } .test-cases-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } .search-filters { display: flex; gap: 12px; } .test-case-item, .history-item { display: flex; justify-content: space-between; align-items: center; padding: 16px; border: 1px solid var(--el-border-color-light); border-radius: 6px; margin-bottom: 8px; background: white; } .test-case-info, .history-info { flex: 1; } .test-case-name, .history-tool { font-weight: 600; color: var(--el-text-color-primary); margin-bottom: 4px; } .test-case-meta, .history-time { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--el-text-color-secondary); } .test-case-date { margin-left: auto; } .test-case-actions, .history-actions { display: flex; gap: 8px; } .history-result { display: flex; align-items: center; gap: 8px; margin-right: 16px; } .no-tool-selected { flex: 1; display: flex; align-items: center; justify-content: center; background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); } .empty-state { text-align: center; padding: 40px 20px; } .history-details .detail-section { margin-bottom: 20px; } .history-details .detail-section h4 { margin: 0 0 8px 0; color: var(--el-text-color-primary); font-size: 14px; font-weight: 600; } .history-details .detail-content p { margin: 4px 0; font-size: 14px; } @media (max-width: 768px) { .main-content { flex-direction: column; } .tools-panel { width: 100%; } .header-section { flex-direction: column; gap: 12px; align-items: stretch; } .search-filters { flex-direction: column; } } </style>

Latest Blog Posts

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/zaizaizhao/mcp-swagger-server'

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