@myatkyawthu/mcp-connect 0.2.0 → 0.3.0

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.
package/package.json CHANGED
@@ -1,63 +1,59 @@
1
- {
2
- "name": "@myatkyawthu/mcp-connect",
3
- "version": "0.2.0",
4
- "description": "Dead simple MCP (Model Context Protocol) server for exposing your app functions to AI agents",
5
- "type": "module",
6
- "main": "./src/index.js",
7
- "module": "./src/index.js",
8
- "exports": {
9
- ".": {
10
- "import": "./src/index.js",
11
- "require": "./src/index.js"
12
- }
13
- },
14
- "bin": {
15
- "mcp-connect": "./src/cli.js"
16
- },
17
- "files": [
18
- "src",
19
- "README.md",
20
- "LICENSE"
21
- ],
22
- "scripts": {
23
- "start": "node src/cli.js",
24
- "dev": "nodemon src/cli.js",
25
- "test": "node --test",
26
- "test:unit": "node --test tests/unit",
27
- "test:integration": "node --test tests/integration",
28
- "lint": "eslint src/",
29
- "format": "prettier --write src/"
30
- },
31
- "keywords": [
32
- "mcp",
33
- "model-context-protocol",
34
- "ai",
35
- "agents",
36
- "tools",
37
- "claude",
38
- "gpt",
39
- "javascript",
40
- "nodejs"
41
- ],
42
- "author": "myat-kyaw-thu",
43
- "license": "MIT",
44
- "repository": {
45
- "type": "git",
46
- "url": "https://github.com/myat-kyaw-thu/MCP_Indigration_Package-NPM.git"
47
- },
48
- "bugs": {
49
- "url": "https://github.com/myat-kyaw-thu/MCP_Indigration_Package-NPM/issues"
50
- },
51
- "homepage": "https://github.com/myat-kyaw-thu/MCP_Indigration_Package-NPM#readme",
52
- "engines": {
53
- "node": ">=18.0.0"
54
- },
55
- "dependencies": {
56
- "@modelcontextprotocol/sdk": "^0.5.0"
57
- },
58
- "devDependencies": {
59
- "nodemon": "^3.0.0",
60
- "eslint": "^8.0.0",
61
- "prettier": "^3.0.0"
62
- }
63
- }
1
+ {
2
+ "name": "@myatkyawthu/mcp-connect",
3
+ "version": "0.3.0",
4
+ "description": "Dead simple MCP (Model Context Protocol) server for exposing your app functions to AI agents",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "bin": {
11
+ "mcp-connect": "./src/cli.js"
12
+ },
13
+ "files": [
14
+ "src",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "start": "node src/cli.js",
20
+ "dev": "nodemon src/cli.js",
21
+ "test": "node --test",
22
+ "test:unit": "node --test tests/unit/*.test.js",
23
+ "test:integration": "node --test tests/integration",
24
+ "lint": "eslint src/",
25
+ "format": "prettier --write src/"
26
+ },
27
+ "keywords": [
28
+ "mcp",
29
+ "model-context-protocol",
30
+ "ai",
31
+ "agents",
32
+ "tools",
33
+ "claude",
34
+ "gpt",
35
+ "javascript",
36
+ "nodejs"
37
+ ],
38
+ "author": "myat-kyaw-thu",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/myat-kyaw-thu/MCP_Indigration_Package-NPM.git"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/myat-kyaw-thu/MCP_Indigration_Package-NPM/issues"
46
+ },
47
+ "homepage": "https://github.com/myat-kyaw-thu/MCP_Indigration_Package-NPM#readme",
48
+ "engines": {
49
+ "node": ">=18.0.0"
50
+ },
51
+ "dependencies": {
52
+ "@modelcontextprotocol/sdk": "^1.29.0"
53
+ },
54
+ "devDependencies": {
55
+ "eslint": "^8.0.0",
56
+ "nodemon": "^3.0.0",
57
+ "prettier": "^3.0.0"
58
+ }
59
+ }
package/src/cli.js CHANGED
@@ -5,9 +5,6 @@ import { resolve } from 'path';
5
5
  import { defineMCP } from './defineMCP.js';
6
6
  import { MCPConnectServer } from './server/mcpServer.js';
7
7
 
8
- /**
9
- * Handle init command - create sample config file
10
- */
11
8
  async function handleInitCommand() {
12
9
  const configPath = resolve(process.cwd(), 'mcp.config.js');
13
10
 
@@ -26,7 +23,7 @@ export default defineMCP({
26
23
  tools: [
27
24
  // Simple tuple format: [name, handler]
28
25
  ["hello", async ({ name = "World" }) => \`Hello \${name}!\`],
29
-
26
+
30
27
  // Object format with schema validation
31
28
  {
32
29
  name: "echo",
@@ -68,27 +65,40 @@ export default defineMCP({
68
65
  }
69
66
  }
70
67
 
71
- /**
72
- * CLI entry point for mcp-connect
73
- * Uses MCP SDK with STDIO transport or Express.js for HTTP transport
74
- */
75
68
  async function main() {
76
69
  try {
77
- // Handle init command
78
70
  if (process.argv[2] === 'init') {
79
71
  await handleInitCommand();
80
72
  return;
81
73
  }
82
74
 
75
+ if (process.argv[2] === 'relay') {
76
+ const portIndex = process.argv.indexOf('--port');
77
+ const relayPort = portIndex !== -1 ? parseInt(process.argv[portIndex + 1], 10) : 4000;
78
+ const { MCPRelayServer } = await import('./server/relay.js');
79
+ const relay = new MCPRelayServer();
80
+ await relay.start(relayPort);
81
+ return;
82
+ }
83
+
83
84
  console.error('Starting MCP-Connect CLI...');
84
85
 
85
- // Check if config file path is provided as argument
86
- const configArg = process.argv[2];
87
- /** @type {string|null} */
86
+ // Parse --port flag
87
+ let port = null;
88
+ const portIndex = process.argv.indexOf('--port');
89
+ if (portIndex !== -1) {
90
+ port = parseInt(process.argv[portIndex + 1], 10);
91
+ if (!port || port < 1 || port > 65535) {
92
+ console.error('Invalid port. Usage: mcp-connect --port 3000');
93
+ process.exit(1);
94
+ }
95
+ }
96
+
97
+ // Find config path (skip flags)
88
98
  let configPath = null;
99
+ const configArg = process.argv.find((a, i) => i >= 2 && !a.startsWith('--') && process.argv[i - 1] !== '--port' && a !== 'relay');
89
100
 
90
101
  if (configArg) {
91
- // Config file path provided as argument
92
102
  const fullPath = resolve(configArg);
93
103
  if (existsSync(fullPath)) {
94
104
  configPath = fullPath;
@@ -97,7 +107,6 @@ async function main() {
97
107
  process.exit(1);
98
108
  }
99
109
  } else {
100
- // Look for config file in current directory (prioritize .js and .mjs)
101
110
  const configPaths = ['mcp.config.js', 'mcp.config.mjs', 'mcp.config.ts'];
102
111
 
103
112
  for (const path of configPaths) {
@@ -134,7 +143,6 @@ export default defineMCP({
134
143
 
135
144
  let configModule;
136
145
  try {
137
- // Handle TypeScript config files
138
146
  if (configPath.endsWith('.ts')) {
139
147
  console.error('TypeScript config detected. For Node.js compatibility:');
140
148
  console.error('1. Rename mcp.config.ts to mcp.config.js and convert to JavaScript');
@@ -157,7 +165,6 @@ export default defineMCP({
157
165
  process.exit(1);
158
166
  }
159
167
 
160
- // Import config file using Node.js ES modules
161
168
  const fileUrl = configPath.startsWith('/')
162
169
  ? `file://${configPath}`
163
170
  : `file:///${configPath.replace(/\\/g, '/')}`;
@@ -172,7 +179,7 @@ export default defineMCP({
172
179
  error.message.includes('Cannot resolve') ||
173
180
  error.message.includes('MODULE_NOT_FOUND')
174
181
  ) {
175
- console.error("Make sure 'mcp-connect' is installed: npm install mcp-connect");
182
+ console.error('Make sure \'@myatkyawthu/mcp-connect\' is installed: npm install mcp-connect');
176
183
  } else if (error.message.includes('SyntaxError')) {
177
184
  console.error('Config file has syntax errors. Check your JavaScript syntax.');
178
185
  } else if (error.message.includes('ERR_MODULE_NOT_FOUND')) {
@@ -190,7 +197,6 @@ export default defineMCP({
190
197
  process.exit(1);
191
198
  }
192
199
 
193
- // Validate config using defineMCP (in case user didn't use it)
194
200
  let validatedConfig;
195
201
  try {
196
202
  validatedConfig = typeof config === 'function' ? config : defineMCP(config);
@@ -199,11 +205,25 @@ export default defineMCP({
199
205
  process.exit(1);
200
206
  }
201
207
 
202
- // Create and start MCP server
203
208
  const server = new MCPConnectServer(validatedConfig);
204
- await server.start();
205
209
 
206
- // Handle graceful shutdown
210
+ const finalPort = port || validatedConfig.server?.port;
211
+ const useSSE = !!finalPort || validatedConfig.server?.transport === 'sse' || validatedConfig.server?.transport === 'hybrid';
212
+ const useTunnel = process.argv.includes('--tunnel') || !!validatedConfig.server?.tunnel?.enabled;
213
+ const relayUrl = process.env.MCP_RELAY_URL || validatedConfig.server?.tunnel?.relayUrl || 'http://localhost:4000';
214
+
215
+ if (useSSE) {
216
+ await server.startSSE(finalPort || 3000);
217
+ } else {
218
+ await server.start();
219
+ }
220
+
221
+ if (useTunnel) {
222
+ server.startTunnel(relayUrl).catch((err) => {
223
+ console.error('Tunnel failed to start:', err.message);
224
+ });
225
+ }
226
+
207
227
  process.on('SIGINT', async () => {
208
228
  console.error('Shutting down...');
209
229
  try {
@@ -229,8 +249,6 @@ export default defineMCP({
229
249
  }
230
250
  }
231
251
 
232
- // Run CLI if this file is executed directly
233
- // Multiple checks to ensure main() runs when CLI is executed
234
252
  const isMainModule =
235
253
  process.argv[1] &&
236
254
  (import.meta.url === `file://${process.argv[1]}` ||
package/src/defineMCP.js CHANGED
@@ -1,21 +1,7 @@
1
1
  import { formatValidationErrors, validateConfig } from './utils/configValidation.js';
2
2
  import { logger } from './utils/logger.js';
3
3
 
4
- /**
5
- * Define MCP configuration with tools and server settings
6
- * This is the main user-facing API that converts simple tool definitions
7
- * into MCP-compliant format with comprehensive validation
8
- *
9
- * @param {Object} config - Configuration object
10
- * @param {string} config.name - Server name
11
- * @param {string} config.version - Server version
12
- * @param {string} [config.description] - Server description
13
- * @param {import('./types/mcp.js').ToolDefinition[]} config.tools - Tool definitions
14
- * @returns {import('./types/mcp.js').MCPConfig} MCP configuration object
15
- * @throws {Error} If configuration is invalid
16
- */
17
4
  export function defineMCP(config) {
18
- // Comprehensive configuration validation
19
5
  const validationResult = validateConfig(config);
20
6
 
21
7
  if (!validationResult.isValid) {
@@ -24,7 +10,6 @@ export function defineMCP(config) {
24
10
  throw new Error(`Invalid MCP configuration:\n${errorMessage}`);
25
11
  }
26
12
 
27
- // Log warnings if any
28
13
  if (validationResult.warnings.length > 0) {
29
14
  const warningMessage = formatValidationErrors({
30
15
  isValid: true,
@@ -34,11 +19,8 @@ export function defineMCP(config) {
34
19
  logger.warn('Configuration warnings', warningMessage);
35
20
  }
36
21
 
37
- // Convert validated tool definitions to MCP format
38
- /** @type {import('./types/mcp.js').MCPTool[]} */
39
22
  const mcpTools = config.tools.map((tool) => {
40
23
  if (Array.isArray(tool)) {
41
- // Handle [name, function] format
42
24
  const [name, handler] = tool;
43
25
  return {
44
26
  name: name.trim(),
@@ -51,8 +33,7 @@ export function defineMCP(config) {
51
33
  handler,
52
34
  };
53
35
  } else {
54
- // Handle object format
55
- const { name, handler, description, schema } = tool;
36
+ const { name, handler, description, schema, confirm } = tool;
56
37
  return {
57
38
  name: name.trim(),
58
39
  description: description || `Tool: ${name}`,
@@ -62,15 +43,16 @@ export function defineMCP(config) {
62
43
  additionalProperties: true,
63
44
  },
64
45
  handler,
46
+ confirm: !!confirm,
65
47
  };
66
48
  }
67
49
  });
68
50
 
69
- // Return MCP-compliant config
70
51
  return {
71
52
  name: config.name,
72
53
  version: config.version,
73
54
  description: config.description,
55
+ server: config.server || {},
74
56
  tools: mcpTools,
75
57
  };
76
58
  }
package/src/index.js CHANGED
@@ -1,33 +1,5 @@
1
- /**
2
- * MCP-Connect - Dead simple MCP (Model Context Protocol) server for exposing your app functions to AI agents
3
- *
4
- * Main entry point - exports public API
5
- *
6
- * @example
7
- * ```javascript
8
- * import { defineMCP } from 'mcp-connect';
9
- *
10
- * export default defineMCP({
11
- * name: 'My App',
12
- * version: '1.0.0',
13
- * tools: [
14
- * ['hello', async ({ name }) => `Hello ${name}!`]
15
- * ]
16
- * });
17
- * ```
18
- */
19
-
20
- // Main API exports
21
1
  export { defineMCP } from './defineMCP.js';
22
2
  export { MCPConnectServer } from './server/mcpServer.js';
23
-
24
- // Type validation utilities (for runtime type checking)
3
+ export { MCPRelayServer } from './server/relay.js';
4
+ export { encrypt, decrypt } from './utils/crypto.js';
25
5
  export { isValidMCPConfig, isValidMCPTool, isValidToolDefinition } from './types/mcp.js';
26
-
27
- /**
28
- * @typedef {import('./types/mcp.js').MCPConfig} MCPConfig
29
- * @typedef {import('./types/mcp.js').MCPTool} MCPTool
30
- * @typedef {import('./types/mcp.js').ToolDefinition} ToolDefinition
31
- * @typedef {import('./types/mcp.js').MCPServerOptions} MCPServerOptions
32
- * @typedef {import('./types/mcp.js').ToolFunction} ToolFunction
33
- */
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Returns the inspector dashboard HTML.
3
+ * Self-contained: inline CSS + JS, no external deps.
4
+ */
5
+ export function getDashboardHTML(serverName, tools) {
6
+ const toolListJSON = JSON.stringify(tools.map(t => ({
7
+ name: t.name,
8
+ description: t.description,
9
+ inputSchema: t.inputSchema,
10
+ })));
11
+
12
+ return `<!DOCTYPE html>
13
+ <html lang="en">
14
+ <head>
15
+ <meta charset="utf-8">
16
+ <meta name="viewport" content="width=device-width, initial-scale=1">
17
+ <title>${serverName} — MCP Inspector</title>
18
+ <style>
19
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
20
+ :root {
21
+ --bg: #0f1117; --surface: #1a1d27; --border: #2a2d3a;
22
+ --text: #e1e4ed; --text-dim: #8b8fa3; --accent: #6c63ff;
23
+ --green: #34d399; --red: #f87171; --yellow: #fbbf24;
24
+ --font: 'SF Mono', 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
25
+ }
26
+ body { font-family: var(--font); background: var(--bg); color: var(--text); font-size: 13px; height: 100vh; display: flex; flex-direction: column; }
27
+ header { padding: 12px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px; background: var(--surface); }
28
+ header h1 { font-size: 14px; font-weight: 600; }
29
+ header .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; }
30
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .4; } }
31
+ header .meta { margin-left: auto; color: var(--text-dim); font-size: 11px; }
32
+
33
+ .container { display: flex; flex: 1; overflow: hidden; }
34
+
35
+ /* Sidebar */
36
+ .sidebar { width: 280px; border-right: 1px solid var(--border); display: flex; flex-direction: column; background: var(--surface); }
37
+ .sidebar h2 { padding: 12px 16px 8px; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: var(--text-dim); }
38
+ .tool-list { flex: 1; overflow-y: auto; }
39
+ .tool-item { padding: 10px 16px; border-bottom: 1px solid var(--border); cursor: pointer; transition: background .15s; }
40
+ .tool-item:hover { background: rgba(108,99,255,.1); }
41
+ .tool-item .name { font-weight: 600; font-size: 13px; }
42
+ .tool-item .desc { color: var(--text-dim); font-size: 11px; margin-top: 2px; }
43
+
44
+ /* Main panel */
45
+ .main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
46
+ .tabs { display: flex; border-bottom: 1px solid var(--border); background: var(--surface); }
47
+ .tab { padding: 10px 20px; cursor: pointer; font-size: 12px; color: var(--text-dim); border-bottom: 2px solid transparent; transition: all .15s; }
48
+ .tab.active { color: var(--accent); border-bottom-color: var(--accent); }
49
+
50
+ /* Logs panel */
51
+ .logs { flex: 1; overflow-y: auto; padding: 0; }
52
+ .log-entry { display: flex; align-items: center; gap: 12px; padding: 8px 16px; border-bottom: 1px solid var(--border); font-size: 12px; cursor: pointer; transition: background .1s; }
53
+ .log-entry:hover { background: rgba(108,99,255,.05); }
54
+ .log-entry.selected { background: rgba(108,99,255,.1); }
55
+ .log-time { color: var(--text-dim); white-space: nowrap; }
56
+ .log-method { background: var(--accent); color: #fff; padding: 2px 8px; border-radius: 3px; font-size: 10px; font-weight: 600; }
57
+ .log-tool { font-weight: 600; }
58
+ .log-duration { color: var(--text-dim); margin-left: auto; white-space: nowrap; }
59
+ .log-status { width: 8px; height: 8px; border-radius: 50%; }
60
+ .log-status.ok { background: var(--green); }
61
+ .log-status.err { background: var(--red); }
62
+ .empty-state { display: flex; align-items: center; justify-content: center; flex: 1; color: var(--text-dim); font-size: 14px; }
63
+
64
+ /* Detail panel */
65
+ .detail { border-top: 1px solid var(--border); max-height: 40%; overflow-y: auto; padding: 16px; background: var(--surface); display: none; }
66
+ .detail.visible { display: block; }
67
+ .detail h3 { font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: var(--text-dim); margin-bottom: 8px; }
68
+ .detail pre { background: var(--bg); padding: 12px; border-radius: 6px; overflow-x: auto; font-size: 12px; margin-bottom: 16px; white-space: pre-wrap; word-break: break-all; }
69
+
70
+ /* Test panel */
71
+ .test-panel { flex: 1; padding: 20px; overflow-y: auto; display: none; }
72
+ .test-panel.visible { display: block; }
73
+ .test-panel label { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: var(--text-dim); margin-bottom: 6px; }
74
+ .test-panel select, .test-panel textarea { width: 100%; background: var(--bg); border: 1px solid var(--border); color: var(--text); font-family: var(--font); font-size: 13px; padding: 10px; border-radius: 6px; margin-bottom: 16px; outline: none; }
75
+ .test-panel select:focus, .test-panel textarea:focus { border-color: var(--accent); }
76
+ .test-panel textarea { min-height: 100px; resize: vertical; }
77
+ .test-btn { background: var(--accent); color: #fff; border: none; padding: 10px 24px; border-radius: 6px; font-family: var(--font); font-size: 13px; font-weight: 600; cursor: pointer; transition: opacity .15s; }
78
+ .test-btn:hover { opacity: .85; }
79
+ .test-btn:disabled { opacity: .5; cursor: not-allowed; }
80
+ .test-result { margin-top: 16px; }
81
+ .test-result pre { background: var(--bg); padding: 12px; border-radius: 6px; font-size: 12px; white-space: pre-wrap; word-break: break-all; }
82
+ .counter { background: var(--border); color: var(--text-dim); padding: 2px 8px; border-radius: 10px; font-size: 10px; margin-left: 6px; }
83
+ </style>
84
+ </head>
85
+ <body>
86
+ <header>
87
+ <div class="dot"></div>
88
+ <h1>${serverName}</h1>
89
+ <span class="meta">MCP Inspector</span>
90
+ </header>
91
+ <div class="container">
92
+ <div class="sidebar">
93
+ <h2>Tools</h2>
94
+ <div class="tool-list" id="toolList"></div>
95
+ </div>
96
+ <div class="main">
97
+ <div class="tabs">
98
+ <div class="tab active" data-tab="logs">Logs <span class="counter" id="logCount">0</span></div>
99
+ <div class="tab" data-tab="test">Test Tool</div>
100
+ </div>
101
+ <div class="logs" id="logsPanel">
102
+ <div class="empty-state" id="emptyState">Waiting for requests…</div>
103
+ </div>
104
+ <div class="detail" id="detailPanel"></div>
105
+ <div class="test-panel" id="testPanel">
106
+ <label for="toolSelect">Tool</label>
107
+ <select id="toolSelect"></select>
108
+ <label for="argsInput">Arguments (JSON)</label>
109
+ <textarea id="argsInput">{}</textarea>
110
+ <button class="test-btn" id="testBtn" onclick="runTest()">Execute</button>
111
+ <div class="test-result" id="testResult"></div>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ <script>
116
+ const TOOLS = ${toolListJSON};
117
+ let logs = [];
118
+ let selectedLogIndex = -1;
119
+
120
+ // Render tool sidebar
121
+ const toolList = document.getElementById('toolList');
122
+ TOOLS.forEach(t => {
123
+ const el = document.createElement('div');
124
+ el.className = 'tool-item';
125
+ el.innerHTML = '<div class="name">' + esc(t.name) + '</div><div class="desc">' + esc(t.description || '') + '</div>';
126
+ el.onclick = () => { document.getElementById('toolSelect').value = t.name; switchTab('test'); };
127
+ toolList.appendChild(el);
128
+ });
129
+
130
+ // Render tool select
131
+ const sel = document.getElementById('toolSelect');
132
+ TOOLS.forEach(t => { const o = document.createElement('option'); o.value = t.name; o.textContent = t.name; sel.appendChild(o); });
133
+ sel.onchange = () => {
134
+ const tool = TOOLS.find(t => t.name === sel.value);
135
+ if (tool && tool.inputSchema && tool.inputSchema.properties) {
136
+ const example = {};
137
+ for (const [k, v] of Object.entries(tool.inputSchema.properties)) { example[k] = v.type === 'number' ? 0 : ''; }
138
+ document.getElementById('argsInput').value = JSON.stringify(example, null, 2);
139
+ }
140
+ };
141
+ if (TOOLS.length) sel.dispatchEvent(new Event('change'));
142
+
143
+ // Tabs
144
+ document.querySelectorAll('.tab').forEach(tab => tab.onclick = () => switchTab(tab.dataset.tab));
145
+ function switchTab(name) {
146
+ document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === name));
147
+ document.getElementById('logsPanel').style.display = name === 'logs' ? '' : 'none';
148
+ document.getElementById('detailPanel').style.display = name === 'logs' && selectedLogIndex >= 0 ? 'block' : 'none';
149
+ document.getElementById('testPanel').style.display = name === 'test' ? 'block' : 'none';
150
+ }
151
+
152
+ // SSE log stream
153
+ const evtSrc = new EventSource('/api/events');
154
+ evtSrc.onmessage = (e) => {
155
+ const entry = JSON.parse(e.data);
156
+ logs.push(entry);
157
+ document.getElementById('logCount').textContent = logs.length;
158
+ document.getElementById('emptyState')?.remove();
159
+ appendLogRow(entry, logs.length - 1);
160
+ };
161
+
162
+ function appendLogRow(entry, index) {
163
+ const panel = document.getElementById('logsPanel');
164
+ const row = document.createElement('div');
165
+ row.className = 'log-entry';
166
+ const time = new Date(entry.timestamp).toLocaleTimeString();
167
+ row.innerHTML =
168
+ '<span class="log-time">' + time + '</span>' +
169
+ '<span class="log-method">CALL</span>' +
170
+ '<span class="log-tool">' + esc(entry.tool) + '</span>' +
171
+ '<span class="log-duration">' + entry.duration + 'ms</span>' +
172
+ '<div class="log-status ' + (entry.error ? 'err' : 'ok') + '"></div>';
173
+ row.onclick = () => showDetail(index, row);
174
+ panel.appendChild(row);
175
+ panel.scrollTop = panel.scrollHeight;
176
+ }
177
+
178
+ function showDetail(index, rowEl) {
179
+ selectedLogIndex = index;
180
+ const entry = logs[index];
181
+ document.querySelectorAll('.log-entry').forEach(r => r.classList.remove('selected'));
182
+ if (rowEl) rowEl.classList.add('selected');
183
+ const detail = document.getElementById('detailPanel');
184
+ detail.className = 'detail visible';
185
+ detail.innerHTML =
186
+ '<h3>Request</h3><pre>' + esc(JSON.stringify(entry.args, null, 2)) + '</pre>' +
187
+ '<h3>' + (entry.error ? 'Error' : 'Response') + '</h3><pre>' + esc(entry.error || entry.response || 'null') + '</pre>';
188
+ }
189
+
190
+ // Test tool execution
191
+ async function runTest() {
192
+ const btn = document.getElementById('testBtn');
193
+ const resultDiv = document.getElementById('testResult');
194
+ const tool = document.getElementById('toolSelect').value;
195
+ let args;
196
+ try { args = JSON.parse(document.getElementById('argsInput').value); } catch { resultDiv.innerHTML = '<pre style="color:var(--red)">Invalid JSON</pre>'; return; }
197
+ btn.disabled = true; btn.textContent = 'Running…';
198
+ try {
199
+ const res = await fetch('/api/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tool, args }) });
200
+ const data = await res.json();
201
+ resultDiv.innerHTML = '<pre>' + esc(JSON.stringify(data, null, 2)) + '</pre>';
202
+ } catch (err) {
203
+ resultDiv.innerHTML = '<pre style="color:var(--red)">' + esc(err.message) + '</pre>';
204
+ }
205
+ btn.disabled = false; btn.textContent = 'Execute';
206
+ }
207
+
208
+ function esc(s) { const d = document.createElement('div'); d.textContent = String(s ?? ''); return d.innerHTML; }
209
+ </script>
210
+ </body>
211
+ </html>`;
212
+ }