455 lines
18 KiB
HTML
455 lines
18 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>HuLa MCP 测试客户端</title>
|
||
<style>
|
||
body {
|
||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
line-height: 1.6;
|
||
}
|
||
h1, h2 {
|
||
color: #333;
|
||
}
|
||
.container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
.card {
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
}
|
||
.log-container {
|
||
background-color: #f5f5f5;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
padding: 10px;
|
||
height: 300px;
|
||
overflow-y: auto;
|
||
font-family: monospace;
|
||
margin-top: 10px;
|
||
}
|
||
.log-entry {
|
||
margin: 5px 0;
|
||
word-break: break-all;
|
||
}
|
||
.log-entry.request {
|
||
color: #0066cc;
|
||
}
|
||
.log-entry.response {
|
||
color: #009900;
|
||
}
|
||
.log-entry.error {
|
||
color: #cc0000;
|
||
}
|
||
button {
|
||
background-color: #4CAF50;
|
||
color: white;
|
||
border: none;
|
||
padding: 10px 15px;
|
||
text-align: center;
|
||
text-decoration: none;
|
||
display: inline-block;
|
||
font-size: 16px;
|
||
margin: 4px 2px;
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
}
|
||
button:hover {
|
||
background-color: #45a049;
|
||
}
|
||
input, select {
|
||
padding: 8px;
|
||
margin: 5px 0;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
width: 100%;
|
||
}
|
||
.form-group {
|
||
margin-bottom: 10px;
|
||
}
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
font-weight: bold;
|
||
}
|
||
.json-viewer {
|
||
background-color: #f8f8f8;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
padding: 10px;
|
||
margin-top: 10px;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
font-family: monospace;
|
||
white-space: pre-wrap;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>HuLa MCP 测试客户端</h1>
|
||
|
||
<div class="container">
|
||
<div class="card">
|
||
<h2>连接状态</h2>
|
||
<div>
|
||
<span id="connection-status">未连接</span>
|
||
<button id="connect-btn">连接到MCP服务</button>
|
||
<button id="disconnect-btn" disabled>断开连接</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>资源测试</h2>
|
||
<div class="form-group">
|
||
<label for="resource-select">选择资源:</label>
|
||
<select id="resource-select">
|
||
<option value="users://list">用户列表 (users://list)</option>
|
||
<option value="users://online">在线用户 (users://online)</option>
|
||
<option value="groups://list">群组列表 (groups://list)</option>
|
||
<option value="users://user-1">用户详情 (users://user-1)</option>
|
||
<option value="groups://group-1">群组详情 (groups://group-1)</option>
|
||
<option value="users://user-1/conversations">用户会话 (users://user-1/conversations)</option>
|
||
<option value="groups://group-1/members">群组成员 (groups://group-1/members)</option>
|
||
<option value="users://user-1/unread">未读消息数 (users://user-1/unread)</option>
|
||
</select>
|
||
</div>
|
||
<button id="fetch-resource-btn">获取资源</button>
|
||
<div id="resource-result" class="json-viewer" style="display: none;"></div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>工具测试</h2>
|
||
<div class="form-group">
|
||
<label for="tool-select">选择工具:</label>
|
||
<select id="tool-select">
|
||
<option value="send-message">发送消息 (send-message)</option>
|
||
<option value="mark-message-read">标记消息已读 (mark-message-read)</option>
|
||
<option value="update-user-status">更新用户状态 (update-user-status)</option>
|
||
<option value="search-messages">搜索消息 (search-messages)</option>
|
||
</select>
|
||
</div>
|
||
<div id="tool-params-container">
|
||
<!-- 动态生成的参数输入框将显示在这里 -->
|
||
</div>
|
||
<button id="call-tool-btn">调用工具</button>
|
||
<div id="tool-result" class="json-viewer" style="display: none;"></div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>日志</h2>
|
||
<div class="log-container" id="log-container"></div>
|
||
<button id="clear-log-btn">清除日志</button>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 服务器配置
|
||
const SERVER_URL = window.location.origin; // 自动获取当前服务器URL
|
||
const SSE_ENDPOINT = `${SERVER_URL}/sse`;
|
||
const MESSAGES_ENDPOINT = `${SERVER_URL}/messages`;
|
||
|
||
// 全局变量
|
||
let sessionId = null;
|
||
let eventSource = null;
|
||
|
||
// DOM元素
|
||
const connectBtn = document.getElementById('connect-btn');
|
||
const disconnectBtn = document.getElementById('disconnect-btn');
|
||
const connectionStatus = document.getElementById('connection-status');
|
||
const resourceSelect = document.getElementById('resource-select');
|
||
const fetchResourceBtn = document.getElementById('fetch-resource-btn');
|
||
const resourceResult = document.getElementById('resource-result');
|
||
const toolSelect = document.getElementById('tool-select');
|
||
const toolParamsContainer = document.getElementById('tool-params-container');
|
||
const callToolBtn = document.getElementById('call-tool-btn');
|
||
const toolResult = document.getElementById('tool-result');
|
||
const logContainer = document.getElementById('log-container');
|
||
const clearLogBtn = document.getElementById('clear-log-btn');
|
||
|
||
// 工具参数定义
|
||
const toolParams = {
|
||
'send-message': [
|
||
{ name: 'content', type: 'text', label: '消息内容', required: true },
|
||
{ name: 'senderId', type: 'text', label: '发送者ID', required: true, default: 'user-1' },
|
||
{ name: 'receiverId', type: 'text', label: '接收者ID', required: true, default: 'user-2' },
|
||
{ name: 'type', type: 'select', label: '消息类型', options: ['text', 'image', 'file', 'audio', 'video'], default: 'text' },
|
||
{ name: 'isGroup', type: 'checkbox', label: '是否群组消息', default: false }
|
||
],
|
||
'mark-message-read': [
|
||
{ name: 'messageId', type: 'text', label: '消息ID', required: true, default: 'msg-3' },
|
||
{ name: 'userId', type: 'text', label: '用户ID', required: true, default: 'user-1' }
|
||
],
|
||
'update-user-status': [
|
||
{ name: 'userId', type: 'text', label: '用户ID', required: true, default: 'user-1' },
|
||
{ name: 'status', type: 'select', label: '状态', options: ['online', 'offline', 'away'], default: 'online' }
|
||
],
|
||
'search-messages': [
|
||
{ name: 'query', type: 'text', label: '搜索关键词', required: true },
|
||
{ name: 'userId', type: 'text', label: '用户ID', required: true, default: 'user-1' },
|
||
{ name: 'limit', type: 'number', label: '结果数量限制', default: 10 }
|
||
]
|
||
};
|
||
|
||
// 格式化JSON
|
||
function formatJSON(obj) {
|
||
return JSON.stringify(obj, null, 2);
|
||
}
|
||
|
||
// 日志函数
|
||
function log(message, type = 'info') {
|
||
const entry = document.createElement('div');
|
||
entry.className = `log-entry ${type}`;
|
||
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
|
||
logContainer.appendChild(entry);
|
||
logContainer.scrollTop = logContainer.scrollHeight;
|
||
}
|
||
|
||
// 连接到MCP服务
|
||
connectBtn.addEventListener('click', () => {
|
||
if (eventSource) {
|
||
eventSource.close();
|
||
}
|
||
|
||
connectionStatus.textContent = '正在连接...';
|
||
log('正在连接到MCP服务...');
|
||
log(`SSE端点: ${SSE_ENDPOINT}`, 'request');
|
||
|
||
try {
|
||
// 创建SSE连接
|
||
eventSource = new EventSource(SSE_ENDPOINT);
|
||
|
||
eventSource.addEventListener('open', () => {
|
||
log('SSE连接已建立', 'response');
|
||
});
|
||
|
||
eventSource.addEventListener('error', (event) => {
|
||
log(`SSE连接错误`, 'error');
|
||
connectionStatus.textContent = '连接失败';
|
||
connectBtn.disabled = false;
|
||
disconnectBtn.disabled = true;
|
||
|
||
if (eventSource.readyState === EventSource.CLOSED) {
|
||
log('SSE连接已关闭', 'error');
|
||
eventSource = null;
|
||
}
|
||
});
|
||
|
||
eventSource.addEventListener('message', (event) => {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
log(`收到消息: ${JSON.stringify(data)}`, 'response');
|
||
|
||
// 如果收到sessionId,保存它
|
||
if (data.type === 'hello' && data.sessionId) {
|
||
sessionId = data.sessionId;
|
||
connectionStatus.textContent = `已连接 (会话ID: ${sessionId})`;
|
||
connectBtn.disabled = true;
|
||
disconnectBtn.disabled = false;
|
||
log(`已获取会话ID: ${sessionId}`, 'response');
|
||
}
|
||
} catch (error) {
|
||
log(`解析消息失败: ${error.message}`, 'error');
|
||
}
|
||
});
|
||
} catch (error) {
|
||
log(`创建SSE连接失败: ${error.message}`, 'error');
|
||
connectionStatus.textContent = '连接失败';
|
||
}
|
||
});
|
||
|
||
// 断开连接
|
||
disconnectBtn.addEventListener('click', () => {
|
||
if (eventSource) {
|
||
log('正在断开连接...');
|
||
eventSource.close();
|
||
eventSource = null;
|
||
sessionId = null;
|
||
connectionStatus.textContent = '已断开连接';
|
||
connectBtn.disabled = false;
|
||
disconnectBtn.disabled = true;
|
||
log('已断开与MCP服务的连接');
|
||
}
|
||
});
|
||
|
||
// 获取资源
|
||
fetchResourceBtn.addEventListener('click', async () => {
|
||
if (!sessionId) {
|
||
log('请先连接到MCP服务', 'error');
|
||
return;
|
||
}
|
||
|
||
const resourceUri = resourceSelect.value;
|
||
log(`正在获取资源: ${resourceUri}`, 'request');
|
||
resourceResult.style.display = 'none';
|
||
|
||
try {
|
||
const response = await fetch(`${MESSAGES_ENDPOINT}?sessionId=${sessionId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
type: 'getResource',
|
||
uri: resourceUri
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP错误: ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
log(`资源响应已接收`, 'response');
|
||
|
||
// 显示结果
|
||
resourceResult.textContent = formatJSON(data);
|
||
resourceResult.style.display = 'block';
|
||
} catch (error) {
|
||
log(`获取资源失败: ${error.message}`, 'error');
|
||
}
|
||
});
|
||
|
||
// 生成工具参数输入框
|
||
toolSelect.addEventListener('change', () => {
|
||
const selectedTool = toolSelect.value;
|
||
toolParamsContainer.innerHTML = '';
|
||
toolResult.style.display = 'none';
|
||
|
||
if (toolParams[selectedTool]) {
|
||
toolParams[selectedTool].forEach(param => {
|
||
const formGroup = document.createElement('div');
|
||
formGroup.className = 'form-group';
|
||
|
||
const label = document.createElement('label');
|
||
label.textContent = param.label + (param.required ? ' *' : '');
|
||
label.htmlFor = `param-${param.name}`;
|
||
formGroup.appendChild(label);
|
||
|
||
if (param.type === 'select') {
|
||
const select = document.createElement('select');
|
||
select.id = `param-${param.name}`;
|
||
select.name = param.name;
|
||
|
||
param.options.forEach(option => {
|
||
const optionElement = document.createElement('option');
|
||
optionElement.value = option;
|
||
optionElement.textContent = option;
|
||
if (param.default === option) {
|
||
optionElement.selected = true;
|
||
}
|
||
select.appendChild(optionElement);
|
||
});
|
||
|
||
formGroup.appendChild(select);
|
||
} else if (param.type === 'checkbox') {
|
||
const checkbox = document.createElement('input');
|
||
checkbox.type = 'checkbox';
|
||
checkbox.id = `param-${param.name}`;
|
||
checkbox.name = param.name;
|
||
checkbox.checked = param.default || false;
|
||
formGroup.appendChild(checkbox);
|
||
} else {
|
||
const input = document.createElement('input');
|
||
input.type = param.type;
|
||
input.id = `param-${param.name}`;
|
||
input.name = param.name;
|
||
if (param.default !== undefined) {
|
||
input.value = param.default;
|
||
}
|
||
formGroup.appendChild(input);
|
||
}
|
||
|
||
toolParamsContainer.appendChild(formGroup);
|
||
});
|
||
}
|
||
});
|
||
|
||
// 初始化工具参数输入框
|
||
toolSelect.dispatchEvent(new Event('change'));
|
||
|
||
// 调用工具
|
||
callToolBtn.addEventListener('click', async () => {
|
||
if (!sessionId) {
|
||
log('请先连接到MCP服务', 'error');
|
||
return;
|
||
}
|
||
|
||
const selectedTool = toolSelect.value;
|
||
const params = {};
|
||
let hasError = false;
|
||
|
||
if (toolParams[selectedTool]) {
|
||
toolParams[selectedTool].forEach(param => {
|
||
const element = document.getElementById(`param-${param.name}`);
|
||
|
||
if (param.type === 'checkbox') {
|
||
params[param.name] = element.checked;
|
||
} else if (param.type === 'number') {
|
||
params[param.name] = Number(element.value);
|
||
} else {
|
||
params[param.name] = element.value;
|
||
}
|
||
|
||
if (param.required && !params[param.name] && params[param.name] !== false) {
|
||
log(`参数 ${param.name} 是必填的`, 'error');
|
||
hasError = true;
|
||
}
|
||
});
|
||
}
|
||
|
||
if (hasError) return;
|
||
toolResult.style.display = 'none';
|
||
|
||
log(`正在调用工具: ${selectedTool},参数: ${JSON.stringify(params)}`, 'request');
|
||
|
||
try {
|
||
const response = await fetch(`${MESSAGES_ENDPOINT}?sessionId=${sessionId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
type: 'callTool',
|
||
name: selectedTool,
|
||
params: params
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP错误: ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
log(`工具响应已接收`, 'response');
|
||
|
||
// 显示结果
|
||
toolResult.textContent = formatJSON(data);
|
||
toolResult.style.display = 'block';
|
||
} catch (error) {
|
||
log(`调用工具失败: ${error.message}`, 'error');
|
||
}
|
||
});
|
||
|
||
// 清除日志
|
||
clearLogBtn.addEventListener('click', () => {
|
||
logContainer.innerHTML = '';
|
||
log('日志已清除');
|
||
});
|
||
|
||
// 初始化
|
||
log(`服务器URL: ${SERVER_URL}`);
|
||
log(`SSE端点: ${SSE_ENDPOINT}`);
|
||
log(`消息端点: ${MESSAGES_ENDPOINT}`);
|
||
log('测试客户端已准备就绪,请点击"连接到MCP服务"按钮开始测试');
|
||
</script>
|
||
</body>
|
||
</html>
|