@atlisp/mcp 1.8.11 → 1.8.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ import process from 'process';
4
4
  import fs from 'fs';
5
5
  import path from 'path';
6
6
  import crypto from 'crypto';
7
+ import { validateLispCodeWorker, checkParens } from './lisp-security.js';
7
8
 
8
9
  const BUSY_RETRIES = parseInt(process.env.BUSY_RETRIES || '10', 10);
9
10
  const BUSY_DELAY = parseInt(process.env.BUSY_DELAY || '500', 10);
@@ -110,8 +111,8 @@ const cadLaunch = edge.func(function() {/*
110
111
  }
111
112
  }
112
113
  } catch (Exception ex) {
113
- System.Diagnostics.Debug.WriteLine("Create COM " + platforms[i] + " error: " + ex.Message);
114
- }
114
+ System.Diagnostics.Debug.WriteLine("cadLaunch create " + progIds[i] + " error: " + ex.Message);
115
+ }
115
116
  }
116
117
 
117
118
  return new { success = false, error = "No CAD found" };
@@ -470,68 +471,7 @@ async function waitForIdle(platform) {
470
471
  return false;
471
472
  }
472
473
 
473
- const DANGEROUS_PATTERNS = [
474
- { re: /\beval\s*[(\s]/i, msg: '禁止使用 eval 函数' },
475
- { re: /\bload\s*[(\s"]/i, msg: '禁止使用 load 函数' },
476
- { re: /\bstartapp\s*[(\s]/i, msg: '禁止使用 startapp 函数' },
477
- { re: /\bvl-bt\b/i, msg: '禁止使用 vl-bt 函数' },
478
- { re: /\bvl-exit-with-value\b/i, msg: '禁止使用 vl-exit-with-value' },
479
- { re: /\bvl-exit-with-error\b/i, msg: '禁止使用 vl-exit-with-error' },
480
- { re: /\bvlax-import-type-library\b/i, msg: '禁止导入类型库' },
481
- { re: /\bvlax-add-cmd\b/i, msg: '禁止注册新命令' },
482
- { re: /\bvla-Delete\b/i, msg: '禁止直接删除 vla 对象' },
483
- { re: /\bvla-Save\b/i, msg: '禁止直接保存文档' },
484
- { re: /\bcommand\s*["\s]*[_(]?\s*["']?\.?\s*_?(?:quit|exit|close|open|new|recover|audit|purge|wblock|export)/i, msg: '禁止使用危险 CAD 命令' },
485
- { re: /\bdirectory\s*[(\s]/i, msg: '禁止使用 directory 函数' },
486
- { re: /\b(?:read-line|read-char|write-line|write-char)\s*[(\s]/i, msg: '禁止直接文件 I/O' },
487
- { re: /\bvla-deletefile\b/i, msg: '禁止删除文件' },
488
- { re: /\bvla-copyfile\b/i, msg: '禁止复制文件' },
489
- { re: /\bvla-movefile\b/i, msg: '禁止移动文件' },
490
- { re: /\bdos_command_string\s*[(\s]/i, msg: '禁止使用 dos_command_string' },
491
- ];
492
-
493
- function validateLispCode(code) {
494
- const withoutStrings = code.replace(/"([^"\\]*(?:\\.[^"\\]*)*)"/g, '');
495
- const withoutComments = withoutStrings.replace(/;.*$/gm, '');
496
- for (const { re, msg } of DANGEROUS_PATTERNS) {
497
- if (re.test(withoutComments)) {
498
- return { valid: false, error: msg };
499
- }
500
- }
501
- return { valid: true };
502
- }
503
-
504
- export function checkParens(code) {
505
- let depth = 0;
506
- let inString = false;
507
- for (let i = 0; i < code.length; i++) {
508
- const ch = code[i];
509
- if (inString) {
510
- if (ch === '\\' && i + 1 < code.length) {
511
- i++;
512
- continue;
513
- }
514
- if (ch === '"') {
515
- inString = false;
516
- }
517
- continue;
518
- }
519
- if (ch === '"') {
520
- inString = true;
521
- continue;
522
- }
523
- if (ch === ';') {
524
- while (i < code.length && code[i] !== '\n') i++;
525
- continue;
526
- }
527
- if (ch === '(') depth++;
528
- if (ch === ')') depth--;
529
- if (depth < 0) return { valid: false, error: '多余右括号 )', pos: i };
530
- }
531
- if (depth > 0) return { valid: false, error: `缺少 ${depth} 个右括号 )`, pos: code.length - 1 };
532
- if (depth < 0) return { valid: false, error: `多余 ${-depth} 个左括号 (` };
533
- return { valid: true };
534
- }
474
+ export { checkParens } from './lisp-security.js';
535
475
 
536
476
  async function connectToPlatform(targetPlatform) {
537
477
  return new Promise((resolve, reject) => {
@@ -609,7 +549,7 @@ async function handleMessage(msg) {
609
549
  if (!parenCheck.valid) {
610
550
  return { success: false, error: `括号错误: ${parenCheck.error} (位置 ${parenCheck.pos})` };
611
551
  }
612
- const injectionCheck = validateLispCode(msg.code);
552
+ const injectionCheck = validateLispCodeWorker(msg.code);
613
553
  if (!injectionCheck.valid) {
614
554
  return { success: false, error: `安全限制: ${injectionCheck.error}` };
615
555
  }
@@ -662,7 +602,7 @@ async function handleMessage(msg) {
662
602
  if (!parenCheck.valid) {
663
603
  return { success: false, error: `括号错误: ${parenCheck.error} (位置 ${parenCheck.pos})` };
664
604
  }
665
- const injectionCheck = validateLispCode(msg.code);
605
+ const injectionCheck = validateLispCodeWorker(msg.code);
666
606
  if (!injectionCheck.valid) {
667
607
  return { success: false, error: `安全限制: ${injectionCheck.error}` };
668
608
  }
package/dist/config.js ADDED
@@ -0,0 +1,241 @@
1
+ import { z } from 'zod';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+
6
+ const envSchema = z.object({
7
+ PORT: z.string().optional().default('8110'),
8
+ HOST: z.string().optional().default('0.0.0.0'),
9
+ TRANSPORT: z.enum(['http', 'stdio', 'ws']).optional().default('http'),
10
+ MCP_API_KEY: z.string().optional().default(''),
11
+ ENABLE_SSE: z.string().optional().default('true'),
12
+ MESSAGE_TIMEOUT: z.string().optional().default('60000'),
13
+ HEARTBEAT_INTERVAL: z.string().optional().default('30000'),
14
+ BUSY_RETRIES: z.string().optional().default('10'),
15
+ BUSY_DELAY: z.string().optional().default('500'),
16
+ ENABLE_CORS: z.string().optional().default('true'),
17
+ CORS_ORIGIN: z.string().optional().default('*'),
18
+ RATE_LIMIT_WINDOW: z.string().optional().default('60000'),
19
+ RATE_LIMIT_MAX: z.string().optional().default('100'),
20
+ RESOURCE_CACHE_TTL: z.string().optional().default('5000'),
21
+ FUNCTION_LIB_CACHE_TTL: z.string().optional().default('300000'),
22
+ DEBUG: z.string().optional().default('false'),
23
+ DEBUG_FILE: z.string().optional().default(''),
24
+ FUNCTION_LIB_URL: z.string().optional().default('http://s3.atlisp.cn/json/functions.json'),
25
+ CONFIG_FILE: z.string().optional().default(''),
26
+ API_KEY_RATE_LIMIT: z.string().optional().default('1000'),
27
+ METRICS_ENABLED: z.string().optional().default('true'),
28
+ REQUEST_LOG_ENABLED: z.string().optional().default('true'),
29
+ SSL_KEY_PATH: z.string().optional().default(''),
30
+ SSL_CERT_PATH: z.string().optional().default(''),
31
+ LLM_API_KEY: z.string().optional().default(''),
32
+ LLM_API_URL: z.string().optional().default('https://api.openai.com/v1/chat/completions'),
33
+ LLM_MODEL: z.string().optional().default('gpt-4o-mini'),
34
+ LLM_MAX_TOKENS: z.string().optional().default('4096'),
35
+ LLM_TEMPERATURE: z.string().optional().default('0.7'),
36
+ });
37
+
38
+ const configSchema = z.object({
39
+ port: z.number().optional(),
40
+ host: z.string().optional(),
41
+ transport: z.enum(['http', 'stdio', 'ws']).optional(),
42
+ apiKey: z.string().optional(),
43
+ enableSse: z.boolean().optional(),
44
+ messageTimeout: z.number().optional(),
45
+ heartbeatInterval: z.number().optional(),
46
+ busyRetries: z.number().optional(),
47
+ busyDelay: z.number().optional(),
48
+ enableCors: z.boolean().optional(),
49
+ corsOrigin: z.string().optional(),
50
+ rateLimitWindow: z.number().optional(),
51
+ rateLimitMax: z.number().optional(),
52
+ resourceCacheTtl: z.number().optional(),
53
+ functionLibCacheTtl: z.number().optional(),
54
+ debug: z.boolean().optional(),
55
+ debugFile: z.string().optional(),
56
+ functionLibUrl: z.string().optional(),
57
+ configFile: z.string().optional(),
58
+ apiKeyRateLimit: z.number().optional(),
59
+ metricsEnabled: z.boolean().optional(),
60
+ apiKeys: z.array(z.object({
61
+ key: z.string(),
62
+ limit: z.number(),
63
+ name: z.string().optional(),
64
+ role: z.string().optional()
65
+ })).optional(),
66
+ sslKey: z.string().optional(),
67
+ sslCert: z.string().optional(),
68
+ llmApiKey: z.string().optional(),
69
+ llmApiUrl: z.string().optional(),
70
+ llmModel: z.string().optional(),
71
+ llmMaxTokens: z.number().optional(),
72
+ llmTemperature: z.number().optional(),
73
+ maxPoolSize: z.number().optional(),
74
+ });
75
+
76
+ function parseArgs() {
77
+ const args = process.argv.slice(2);
78
+ const result = {};
79
+ for (let i = 0; i < args.length; i++) {
80
+ if (args[i] === '--transport' && args[i + 1]) {
81
+ result.transport = args[i + 1];
82
+ i++;
83
+ } else if (args[i] === '--stdio') {
84
+ result.transport = 'stdio';
85
+ } else if (args[i] === '--ws') {
86
+ result.transport = 'ws';
87
+ } else if (args[i] === '--port' && args[i + 1]) {
88
+ result.port = args[i + 1];
89
+ i++;
90
+ } else if (args[i] === '--host' && args[i + 1]) {
91
+ result.host = args[i + 1];
92
+ i++;
93
+ } else if (args[i] === '--config' && args[i + 1]) {
94
+ result.configFile = args[i + 1];
95
+ i++;
96
+ } else if (args[i] === '--ssl-key' && args[i + 1]) {
97
+ result.sslKey = args[i + 1];
98
+ i++;
99
+ } else if (args[i] === '--ssl-cert' && args[i + 1]) {
100
+ result.sslCert = args[i + 1];
101
+ i++;
102
+ } else if (args[i] === '--debug') {
103
+ result.debug = true;
104
+ }
105
+ }
106
+ return result;
107
+ }
108
+
109
+ export { parseArgs };
110
+
111
+ function loadConfigFile(filePath) {
112
+ if (!filePath || !fs.existsSync(filePath)) return null;
113
+ try {
114
+ const raw = fs.readFileSync(filePath, 'utf-8');
115
+ const parsed = JSON.parse(raw);
116
+ const result = configSchema.safeParse(parsed);
117
+ if (result.success) {
118
+ return result.data;
119
+ }
120
+ console.error('配置文件格式错误:', result.error);
121
+ } catch (e) {
122
+ console.error('加载配置文件失败:', e.message);
123
+ }
124
+ return null;
125
+ }
126
+
127
+ const cliArgs = parseArgs();
128
+ const envResult = envSchema.safeParse(process.env);
129
+
130
+ if (!envResult.success) {
131
+ console.error('环境变量配置错误:', envResult.error.flatten().fieldErrors);
132
+ process.exit(1);
133
+ }
134
+
135
+ const env = envResult.data;
136
+
137
+ const configFilePath = env.CONFIG_FILE || cliArgs.configFile ||
138
+ path.join(os.homedir(), '.atlisp', 'atlisp.config.json');
139
+
140
+ const fileConfig = loadConfigFile(configFilePath);
141
+
142
+ const config = {
143
+ port: fileConfig?.port || parseInt(cliArgs.port || env.PORT, 10),
144
+ host: fileConfig?.host || cliArgs.host || env.HOST,
145
+ transport: fileConfig?.transport ?? cliArgs.transport ?? env.TRANSPORT,
146
+ apiKey: fileConfig?.apiKey || env.MCP_API_KEY,
147
+ enableSse: fileConfig?.enableSse ?? (env.ENABLE_SSE === 'true'),
148
+ messageTimeout: fileConfig?.messageTimeout || parseInt(env.MESSAGE_TIMEOUT, 10),
149
+ heartbeatInterval: fileConfig?.heartbeatInterval || parseInt(env.HEARTBEAT_INTERVAL, 10),
150
+ busyRetries: fileConfig?.busyRetries || parseInt(env.BUSY_RETRIES, 10),
151
+ busyDelay: fileConfig?.busyDelay || parseInt(env.BUSY_DELAY, 10),
152
+ enableCors: fileConfig?.enableCors ?? (env.ENABLE_CORS === 'true'),
153
+ corsOrigin: fileConfig?.corsOrigin || env.CORS_ORIGIN,
154
+ rateLimitWindow: fileConfig?.rateLimitWindow || parseInt(env.RATE_LIMIT_WINDOW, 10),
155
+ rateLimitMax: fileConfig?.rateLimitMax || parseInt(env.RATE_LIMIT_MAX, 10),
156
+ resourceCacheTtl: fileConfig?.resourceCacheTtl || parseInt(env.RESOURCE_CACHE_TTL, 10),
157
+ functionLibCacheTtl: fileConfig?.functionLibCacheTtl || parseInt(env.FUNCTION_LIB_CACHE_TTL, 10),
158
+ debug: fileConfig?.debug ?? (env.DEBUG === 'true'),
159
+ debugFile: fileConfig?.debugFile || env.DEBUG_FILE,
160
+ functionLibUrl: fileConfig?.functionLibUrl || env.FUNCTION_LIB_URL,
161
+ configFile: configFilePath,
162
+ apiKeyRateLimit: fileConfig?.apiKeyRateLimit || parseInt(env.API_KEY_RATE_LIMIT, 10),
163
+ apiKeys: fileConfig?.apiKeys || [],
164
+ metricsEnabled: fileConfig?.metricsEnabled ?? (env.METRICS_ENABLED === 'true'),
165
+ requestLogEnabled: fileConfig?.requestLogEnabled ?? (env.REQUEST_LOG_ENABLED !== 'false'),
166
+ sslKey: fileConfig?.sslKey || env.SSL_KEY_PATH || cliArgs.sslKey,
167
+ sslCert: fileConfig?.sslCert || env.SSL_CERT_PATH || cliArgs.sslCert,
168
+ llmApiKey: fileConfig?.llmApiKey || env.LLM_API_KEY,
169
+ llmApiUrl: fileConfig?.llmApiUrl || env.LLM_API_URL,
170
+ llmModel: fileConfig?.llmModel || env.LLM_MODEL,
171
+ llmMaxTokens: fileConfig?.llmMaxTokens || parseInt(env.LLM_MAX_TOKENS, 10),
172
+ llmTemperature: fileConfig?.llmTemperature || parseFloat(env.LLM_TEMPERATURE),
173
+ maxPoolSize: fileConfig?.maxPoolSize || 3,
174
+ };
175
+
176
+ export function validateConfig() {
177
+ const errors = [];
178
+ if (config.transport === 'http' || config.transport === 'ws') {
179
+ if (config.port < 1 || config.port > 65535) {
180
+ errors.push(`端口号无效: ${config.port}`);
181
+ }
182
+ }
183
+ if (config.sslKey && !fs.existsSync(config.sslKey)) {
184
+ errors.push(`SSL 密钥文件不存在: ${config.sslKey}`);
185
+ }
186
+ if (config.sslCert && !fs.existsSync(config.sslCert)) {
187
+ errors.push(`SSL 证书文件不存在: ${config.sslCert}`);
188
+ }
189
+ if (config.messageTimeout < 1000) {
190
+ errors.push(`消息超时过短: ${config.messageTimeout}ms (最小 1000ms)`);
191
+ }
192
+ if (config.rateLimitMax < 1) {
193
+ errors.push(`速率限制无效: ${config.rateLimitMax}`);
194
+ }
195
+ if (errors.length > 0) {
196
+ console.error('配置验证错误:');
197
+ errors.forEach(e => console.error(` - ${e}`));
198
+ return false;
199
+ }
200
+ return true;
201
+ }
202
+
203
+ let configWatchers = [];
204
+
205
+ if (configFilePath && fs.existsSync(configFilePath)) {
206
+ try {
207
+ fs.watch(configFilePath, (eventType) => {
208
+ if (eventType === 'change') {
209
+ const newFileConfig = loadConfigFile(configFilePath);
210
+ if (newFileConfig) {
211
+ Object.assign(config, newFileConfig);
212
+ configWatchers.forEach(fn => fn(config));
213
+ }
214
+ }
215
+ });
216
+ } catch (e) { console.error(`Config file watch error: ${e.message}`); }
217
+ }
218
+
219
+ export function getConfig(key) {
220
+ return config[key];
221
+ }
222
+
223
+ export function getAllConfig() {
224
+ return { ...config };
225
+ }
226
+
227
+ export function reloadConfig() {
228
+ const newFileConfig = loadConfigFile(config.configFile);
229
+ if (newFileConfig) {
230
+ Object.assign(config, newFileConfig);
231
+ configWatchers.forEach(fn => fn(config));
232
+ return true;
233
+ }
234
+ return false;
235
+ }
236
+
237
+ export function onConfigChange(callback) {
238
+ configWatchers.push(callback);
239
+ }
240
+
241
+ export default config;
@@ -0,0 +1,66 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>@lisp MCP Server API Docs</title>
5
+ <style>
6
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; }
7
+ h1 { color: #333; }
8
+ h2 { color: #666; margin-top: 30px; }
9
+ .endpoint { background: #f5f5f5; padding: 15px; margin: 10px 0; border-radius: 5px; }
10
+ .method { display: inline-block; padding: 3px 8px; border-radius: 3px; font-weight: bold; font-size: 12px; }
11
+ .get { background: #61affe; color: white; }
12
+ .post { background: #49cc90; color: white; }
13
+ code { background: #eee; padding: 2px 5px; border-radius: 3px; }
14
+ .info { background: #f0f0f0; padding: 10px; margin: 10px 0; }
15
+ </style>
16
+ </head>
17
+ <body>
18
+ <h1>@lisp MCP Server API</h1>
19
+ <div class="info">
20
+ <strong>Version:</strong> __VERSION__ |
21
+ <strong>Tools:</strong> __TOOLS_COUNT__ |
22
+ <strong>Resources:</strong> 11
23
+ </div>
24
+
25
+ <h2>Endpoints</h2>
26
+
27
+ <div class="endpoint">
28
+ <span class="method get">GET</span> <code>/health</code> - Health check
29
+ </div>
30
+ <div class="endpoint">
31
+ <span class="method get">GET</span> <code>/metrics</code> - Prometheus metrics
32
+ </div>
33
+ <div class="endpoint">
34
+ <span class="method post">POST</span> <code>/mcp</code> - MCP JSON-RPC endpoint
35
+ </div>
36
+ <div class="endpoint">
37
+ <span class="method get">GET</span> <code>/sse</code> - Server-Sent Events
38
+ </div>
39
+ <div class="endpoint">
40
+ <span class="method post">POST</span> <code>/message</code> - SSE message
41
+ </div>
42
+ <div class="endpoint">
43
+ <span class="method get">GET</span> <code>/debug</code> - Debug panel
44
+ </div>
45
+ <div class="endpoint">
46
+ <span class="method post">POST</span> <code>/config/reload</code> - Reload configuration
47
+ </div>
48
+ <div class="endpoint">
49
+ <span class="method get">GET</span> <code>/rate-limit/status</code> - Rate limit status
50
+ </div>
51
+ <div class="endpoint">
52
+ <span class="method post">POST</span> <code>/rate-limit/reset</code> - Reset rate limit
53
+ </div>
54
+
55
+ <h2>Quick Start</h2>
56
+ <pre>
57
+ # Health check
58
+ curl http://localhost:8110/health
59
+
60
+ # Call MCP tool
61
+ curl -X POST http://localhost:8110/mcp \
62
+ -H "Content-Type: application/json" \
63
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_cad_info","arguments":{}}}'
64
+ </pre>
65
+ </body>
66
+ </html>
@@ -0,0 +1,75 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>@lisp MCP Debug Panel</title>
5
+ <style>
6
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background: #1a1a2e; color: #eee; }
7
+ h1 { color: #00d4ff; }
8
+ h2 { color: #00d4ff; border-bottom: 1px solid #333; padding-bottom: 10px; }
9
+ .card { background: #16213e; padding: 20px; margin: 10px 0; border-radius: 8px; }
10
+ .stat { display: inline-block; margin: 10px 20px; }
11
+ .stat-label { color: #888; font-size: 12px; }
12
+ .stat-value { font-size: 24px; color: #00d4ff; }
13
+ .status-ok { color: #49cc90; }
14
+ .status-error { color: #f93e3e; }
15
+ table { width: 100%; border-collapse: collapse; }
16
+ th, td { padding: 10px; text-align: left; border-bottom: 1px solid #333; }
17
+ th { color: #888; }
18
+ code { background: #0f0f23; padding: 3px 8px; border-radius: 3px; color: #00d4ff; }
19
+ .refresh { background: #00d4ff; color: #1a1a2e; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; margin: 10px 0; }
20
+ </style>
21
+ </head>
22
+ <body>
23
+ <h1>🛠️ @lisp MCP Debug Panel</h1>
24
+ <button class="refresh" onclick="location.reload()">🔄 Refresh</button>
25
+
26
+ <h2>System</h2>
27
+ <div class="card">
28
+ <div class="stat">
29
+ <div class="stat-label">Uptime</div>
30
+ <div class="stat-value">__UPTIME_MIN__m</div>
31
+ </div>
32
+ <div class="stat">
33
+ <div class="stat-label">Memory RSS</div>
34
+ <div class="stat-value">__MEMORY_RSS_MB__ MB</div>
35
+ </div>
36
+ <div class="stat">
37
+ <div class="stat-label">Heap Used</div>
38
+ <div class="stat-value">__HEAP_USED_MB__ MB</div>
39
+ </div>
40
+ <div class="stat">
41
+ <div class="stat-label">MCP Sessions</div>
42
+ <div class="stat-value">__SESSIONS_COUNT__</div>
43
+ </div>
44
+ </div>
45
+
46
+ <h2>CAD Status</h2>
47
+ <div class="card">
48
+ <span class="__CAD_STATUS_CLASS__">
49
+ __CAD_STATUS_TEXT__
50
+ </span>
51
+ </div>
52
+
53
+ <h2>Endpoints</h2>
54
+ <div class="card">
55
+ <table>
56
+ <tr><th>Endpoint</th><th>Method</th><th>Description</th></tr>
57
+ <tr><td><code>/health</code></td><td>GET</td><td>Health check</td></tr>
58
+ <tr><td><code>/metrics</code></td><td>GET</td><td>Prometheus metrics</td></tr>
59
+ <tr><td><code>/mcp</code></td><td>POST</td><td>MCP JSON-RPC</td></tr>
60
+ <tr><td><code>/sse</code></td><td>GET</td><td>Server-Sent Events</td></tr>
61
+ <tr><td><code>/debug</code></td><td>GET</td><td>This panel</td></tr>
62
+ <tr><td><code>/api/docs</code></td><td>GET</td><td>API Documentation</td></tr>
63
+ </table>
64
+ </div>
65
+
66
+ <h2>Tools (__TOOLS_COUNT__)</h2>
67
+ <div class="card">
68
+ <p>Use <code>tools/list</code> MCP method to get full tool list.</p>
69
+ </div>
70
+
71
+ <script>
72
+ setTimeout(() => location.reload(), 30000);
73
+ </script>
74
+ </body>
75
+ </html>