@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/LICENSE +20 -20
- package/README.md +393 -169
- package/package.json +59 -63
- package/src/cli.js +41 -23
- package/src/defineMCP.js +3 -21
- package/src/index.js +2 -30
- package/src/server/dashboard.js +212 -0
- package/src/server/mcpServer.js +460 -145
- package/src/server/relay.js +162 -0
- package/src/types/mcp.js +0 -69
- package/src/utils/configLoader.js +96 -0
- package/src/utils/configValidation.js +84 -123
- package/src/utils/crypto.js +57 -0
- package/src/utils/logger.js +2 -137
- package/src/utils/validation.js +4 -34
package/package.json
CHANGED
|
@@ -1,63 +1,59 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@myatkyawthu/mcp-connect",
|
|
3
|
-
"version": "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
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
},
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|