@mcp-use/inspector 0.2.1 → 0.2.2
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/dist/cli/inspect.js +265 -0
- package/dist/client/assets/{index-Risbd_Le.js → index-DZm4vi0J.js} +1 -1
- package/dist/client/index.html +1 -1
- package/dist/server/favicon-proxy.js +123 -0
- package/dist/server/index.js +7 -0
- package/dist/server/mcp-inspector.js +132 -0
- package/dist/server/middleware.js +145 -0
- package/dist/server/server.js +294 -0
- package/dist/server/standalone.d.ts +9 -0
- package/dist/server/standalone.d.ts.map +1 -0
- package/dist/server/standalone.js +228 -0
- package/dist/server/unified.js +294 -0
- package/package.json +2 -2
package/dist/client/index.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;500;700&display=swap" rel="stylesheet">
|
|
10
10
|
<title>MCP Inspector</title>
|
|
11
|
-
<script type="module" crossorigin src="/inspector/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/inspector/assets/index-DZm4vi0J.js"></script>
|
|
12
12
|
<link rel="stylesheet" crossorigin href="/inspector/assets/index-BOon65c9.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { cors } from 'hono/cors';
|
|
3
|
+
const app = new Hono();
|
|
4
|
+
// Enable CORS for all routes
|
|
5
|
+
app.use('*', cors());
|
|
6
|
+
const faviconCache = new Map();
|
|
7
|
+
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
|
8
|
+
const MAX_CACHE_SIZE = 1000; // Maximum number of cached favicons
|
|
9
|
+
// Clean up expired cache entries
|
|
10
|
+
function cleanupCache() {
|
|
11
|
+
const now = Date.now();
|
|
12
|
+
for (const [key, entry] of faviconCache.entries()) {
|
|
13
|
+
if (now - entry.timestamp > entry.ttl) {
|
|
14
|
+
faviconCache.delete(key);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Run cleanup every hour
|
|
19
|
+
setInterval(cleanupCache, 60 * 60 * 1000);
|
|
20
|
+
// Get cache key for a URL
|
|
21
|
+
function getCacheKey(url) {
|
|
22
|
+
try {
|
|
23
|
+
const urlObj = new URL(url);
|
|
24
|
+
return urlObj.hostname.toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return url.toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Favicon proxy endpoint
|
|
31
|
+
app.get('/:url', async (c) => {
|
|
32
|
+
const url = c.req.param('url');
|
|
33
|
+
if (!url) {
|
|
34
|
+
return c.json({ error: 'URL parameter is required' }, 400);
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
// Decode the URL
|
|
38
|
+
const decodedUrl = decodeURIComponent(url);
|
|
39
|
+
// Add protocol if missing
|
|
40
|
+
let fullUrl = decodedUrl;
|
|
41
|
+
if (!decodedUrl.startsWith('http://') && !decodedUrl.startsWith('https://')) {
|
|
42
|
+
fullUrl = `https://${decodedUrl}`;
|
|
43
|
+
}
|
|
44
|
+
// Validate URL
|
|
45
|
+
const urlObj = new URL(fullUrl);
|
|
46
|
+
const cacheKey = getCacheKey(fullUrl);
|
|
47
|
+
// Check cache first
|
|
48
|
+
const cachedEntry = faviconCache.get(cacheKey);
|
|
49
|
+
if (cachedEntry && (Date.now() - cachedEntry.timestamp) < cachedEntry.ttl) {
|
|
50
|
+
return new Response(cachedEntry.data, {
|
|
51
|
+
headers: {
|
|
52
|
+
'Content-Type': cachedEntry.contentType,
|
|
53
|
+
'Cache-Control': 'public, max-age=86400, immutable', // Cache for 24 hours
|
|
54
|
+
'Access-Control-Allow-Origin': '*',
|
|
55
|
+
'X-Cache': 'HIT',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const protocol = urlObj.protocol;
|
|
60
|
+
const baseDomain = `${protocol}//${urlObj.origin.split('.').slice(-2).join('.')}`;
|
|
61
|
+
// Try to fetch favicon from common locations
|
|
62
|
+
const faviconUrls = [
|
|
63
|
+
`${urlObj.origin}/favicon.ico`,
|
|
64
|
+
`${urlObj.origin}/favicon.png`,
|
|
65
|
+
`${urlObj.origin}/apple-touch-icon.png`,
|
|
66
|
+
`${urlObj.origin}/icon.png`,
|
|
67
|
+
`${baseDomain}/favicon.ico`,
|
|
68
|
+
`${baseDomain}/favicon.png`,
|
|
69
|
+
`${baseDomain}/apple-touch-icon.png`,
|
|
70
|
+
`${baseDomain}/icon.png`,
|
|
71
|
+
];
|
|
72
|
+
for (const faviconUrl of faviconUrls) {
|
|
73
|
+
try {
|
|
74
|
+
const response = await fetch(faviconUrl, {
|
|
75
|
+
headers: {
|
|
76
|
+
'User-Agent': 'Mozilla/5.0 (compatible; MCP-Inspector/1.0)',
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
if (response.ok) {
|
|
80
|
+
const contentType = response.headers.get('content-type') || 'image/x-icon';
|
|
81
|
+
const buffer = await response.arrayBuffer();
|
|
82
|
+
// Cache the result
|
|
83
|
+
if (faviconCache.size >= MAX_CACHE_SIZE) {
|
|
84
|
+
// Remove oldest entries if cache is full
|
|
85
|
+
const entries = Array.from(faviconCache.entries());
|
|
86
|
+
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
87
|
+
const toRemove = entries.slice(0, Math.floor(MAX_CACHE_SIZE / 4));
|
|
88
|
+
toRemove.forEach(([key]) => faviconCache.delete(key));
|
|
89
|
+
}
|
|
90
|
+
faviconCache.set(cacheKey, {
|
|
91
|
+
data: buffer,
|
|
92
|
+
contentType,
|
|
93
|
+
timestamp: Date.now(),
|
|
94
|
+
ttl: CACHE_TTL,
|
|
95
|
+
});
|
|
96
|
+
return new Response(buffer, {
|
|
97
|
+
headers: {
|
|
98
|
+
'Content-Type': contentType,
|
|
99
|
+
'Cache-Control': 'public, max-age=86400, immutable', // Cache for 24 hours
|
|
100
|
+
'Access-Control-Allow-Origin': '*',
|
|
101
|
+
'X-Cache': 'MISS',
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Continue to next URL
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// If no favicon found, return a default icon
|
|
112
|
+
return c.json({ error: 'No favicon found' }, 404);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.error('Favicon proxy error:', error);
|
|
116
|
+
return c.json({ error: 'Invalid URL or fetch failed' }, 400);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
// Health check endpoint
|
|
120
|
+
app.get('/health', (c) => {
|
|
121
|
+
return c.json({ status: 'ok', service: 'favicon-proxy' });
|
|
122
|
+
});
|
|
123
|
+
export default app;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { MCPClient } from 'mcp-use';
|
|
2
|
+
export class MCPInspector {
|
|
3
|
+
constructor() {
|
|
4
|
+
Object.defineProperty(this, "servers", {
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true,
|
|
8
|
+
value: new Map()
|
|
9
|
+
});
|
|
10
|
+
Object.defineProperty(this, "client", {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
writable: true,
|
|
14
|
+
value: void 0
|
|
15
|
+
});
|
|
16
|
+
this.client = new MCPClient();
|
|
17
|
+
}
|
|
18
|
+
async listServers() {
|
|
19
|
+
return Array.from(this.servers.values());
|
|
20
|
+
}
|
|
21
|
+
async connectToServer(url, command) {
|
|
22
|
+
const id = Date.now().toString();
|
|
23
|
+
const name = url || command || 'Unknown Server';
|
|
24
|
+
try {
|
|
25
|
+
// Configure server in MCP client
|
|
26
|
+
const serverName = `server_${id}`;
|
|
27
|
+
const serverConfig = url ? { url } : { command };
|
|
28
|
+
this.client.addServer(serverName, serverConfig);
|
|
29
|
+
// Create session
|
|
30
|
+
const session = await this.client.createSession(serverName, true);
|
|
31
|
+
// Mock tools and resources for now
|
|
32
|
+
const tools = [
|
|
33
|
+
{
|
|
34
|
+
name: 'get_weather',
|
|
35
|
+
description: 'Get current weather for a location',
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
location: { type: 'string', description: 'City name' },
|
|
40
|
+
},
|
|
41
|
+
required: ['location'],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
const resources = [
|
|
46
|
+
{
|
|
47
|
+
uri: 'file:///home/user/documents',
|
|
48
|
+
name: 'Documents',
|
|
49
|
+
description: 'User documents directory',
|
|
50
|
+
mimeType: 'application/x-directory',
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
const server = {
|
|
54
|
+
id,
|
|
55
|
+
name,
|
|
56
|
+
url,
|
|
57
|
+
command,
|
|
58
|
+
status: 'connected',
|
|
59
|
+
session,
|
|
60
|
+
tools,
|
|
61
|
+
resources,
|
|
62
|
+
lastActivity: new Date(),
|
|
63
|
+
};
|
|
64
|
+
this.servers.set(id, server);
|
|
65
|
+
return server;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const server = {
|
|
69
|
+
id,
|
|
70
|
+
name,
|
|
71
|
+
url,
|
|
72
|
+
command,
|
|
73
|
+
status: 'error',
|
|
74
|
+
tools: [],
|
|
75
|
+
resources: [],
|
|
76
|
+
lastActivity: new Date(),
|
|
77
|
+
};
|
|
78
|
+
this.servers.set(id, server);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async getServer(id) {
|
|
83
|
+
return this.servers.get(id) || null;
|
|
84
|
+
}
|
|
85
|
+
async executeTool(serverId, toolName, input) {
|
|
86
|
+
const server = this.servers.get(serverId);
|
|
87
|
+
if (!server || !server.session) {
|
|
88
|
+
throw new Error('Server not found or not connected');
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
// Mock tool execution for now
|
|
92
|
+
const result = {
|
|
93
|
+
tool: toolName,
|
|
94
|
+
input,
|
|
95
|
+
result: `Mock result for ${toolName} with input: ${JSON.stringify(input)}`,
|
|
96
|
+
timestamp: new Date().toISOString(),
|
|
97
|
+
};
|
|
98
|
+
server.lastActivity = new Date();
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
server.status = 'error';
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async getServerTools(serverId) {
|
|
107
|
+
const server = this.servers.get(serverId);
|
|
108
|
+
if (!server) {
|
|
109
|
+
throw new Error('Server not found');
|
|
110
|
+
}
|
|
111
|
+
return server.tools;
|
|
112
|
+
}
|
|
113
|
+
async getServerResources(serverId) {
|
|
114
|
+
const server = this.servers.get(serverId);
|
|
115
|
+
if (!server) {
|
|
116
|
+
throw new Error('Server not found');
|
|
117
|
+
}
|
|
118
|
+
return server.resources;
|
|
119
|
+
}
|
|
120
|
+
async disconnectServer(serverId) {
|
|
121
|
+
const server = this.servers.get(serverId);
|
|
122
|
+
if (server && server.session) {
|
|
123
|
+
try {
|
|
124
|
+
await server.session.disconnect();
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
console.error('Error disconnecting from server:', error);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
this.servers.delete(serverId);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
/**
|
|
8
|
+
* Mount the MCP Inspector UI at a specified path on an Express app
|
|
9
|
+
* Similar to how FastAPI mounts Swagger UI at /docs
|
|
10
|
+
*
|
|
11
|
+
* @param app - Express application instance
|
|
12
|
+
* @param path - Mount path (default: '/inspector')
|
|
13
|
+
* @param mcpServerUrl - Optional MCP server URL to auto-connect to
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { createMCPServer } from 'mcp-use'
|
|
18
|
+
* import { mountInspector } from '@mcp-use/inspector'
|
|
19
|
+
*
|
|
20
|
+
* const server = createMCPServer('my-server')
|
|
21
|
+
* mountInspector(server) // Mounts at /inspector
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function mountInspector(app, path = '/inspector', mcpServerUrl) {
|
|
25
|
+
// Ensure path starts with /
|
|
26
|
+
const basePath = path.startsWith('/') ? path : `/${path}`;
|
|
27
|
+
// Find the built client files
|
|
28
|
+
const clientDistPath = join(__dirname, '../../dist/client');
|
|
29
|
+
if (!existsSync(clientDistPath)) {
|
|
30
|
+
console.warn(`⚠️ MCP Inspector client files not found at ${clientDistPath}`);
|
|
31
|
+
console.warn(` Run 'yarn build' in the inspector package to build the UI`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// Serve inspector config endpoint
|
|
35
|
+
app.get(`${basePath}/config.json`, (_req, res) => {
|
|
36
|
+
res.json({
|
|
37
|
+
autoConnectUrl: mcpServerUrl || null,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
// Favicon proxy endpoint
|
|
41
|
+
app.get(`${basePath}/api/favicon/:url`, async (req, res) => {
|
|
42
|
+
const url = req.params.url;
|
|
43
|
+
if (!url) {
|
|
44
|
+
res.status(400).json({ error: 'URL parameter is required' });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
// Decode the URL
|
|
49
|
+
const decodedUrl = decodeURIComponent(url);
|
|
50
|
+
// Add protocol if missing
|
|
51
|
+
let fullUrl = decodedUrl;
|
|
52
|
+
if (!decodedUrl.startsWith('http://') && !decodedUrl.startsWith('https://')) {
|
|
53
|
+
fullUrl = `https://${decodedUrl}`;
|
|
54
|
+
}
|
|
55
|
+
// Validate URL
|
|
56
|
+
const urlObj = new URL(fullUrl);
|
|
57
|
+
// Try to fetch favicon from common locations
|
|
58
|
+
const faviconUrls = [
|
|
59
|
+
`${urlObj.origin}/favicon.ico`,
|
|
60
|
+
`${urlObj.origin}/favicon.png`,
|
|
61
|
+
`${urlObj.origin}/apple-touch-icon.png`,
|
|
62
|
+
];
|
|
63
|
+
for (const faviconUrl of faviconUrls) {
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch(faviconUrl, {
|
|
66
|
+
headers: {
|
|
67
|
+
'User-Agent': 'Mozilla/5.0 (compatible; MCP-Inspector/1.0)',
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
if (response.ok) {
|
|
71
|
+
const contentType = response.headers.get('content-type') || 'image/x-icon';
|
|
72
|
+
const buffer = await response.arrayBuffer();
|
|
73
|
+
res.setHeader('Content-Type', contentType);
|
|
74
|
+
res.setHeader('Cache-Control', 'public, max-age=86400');
|
|
75
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
76
|
+
res.send(Buffer.from(buffer));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Continue to next URL
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// If no favicon found, return 404
|
|
86
|
+
res.status(404).json({ error: 'No favicon found' });
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.error('Favicon proxy error:', error);
|
|
90
|
+
res.status(400).json({ error: 'Invalid URL or fetch failed' });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
// Serve static assets
|
|
94
|
+
app.use(`${basePath}/assets`, (_req, res) => {
|
|
95
|
+
const assetPath = join(clientDistPath, 'assets', _req.path);
|
|
96
|
+
if (existsSync(assetPath)) {
|
|
97
|
+
// Set appropriate content type
|
|
98
|
+
if (assetPath.endsWith('.js')) {
|
|
99
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
100
|
+
}
|
|
101
|
+
else if (assetPath.endsWith('.css')) {
|
|
102
|
+
res.setHeader('Content-Type', 'text/css');
|
|
103
|
+
}
|
|
104
|
+
res.sendFile(assetPath);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
res.status(404).send('Asset not found');
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
// Handle OAuth callback redirects - redirect /oauth/callback to /inspector/oauth/callback
|
|
111
|
+
// This helps when OAuth providers are configured with the wrong redirect URL
|
|
112
|
+
if (basePath !== '') {
|
|
113
|
+
app.get('/oauth/callback', (req, res) => {
|
|
114
|
+
const queryString = req.url.split('?')[1] || '';
|
|
115
|
+
const redirectUrl = queryString
|
|
116
|
+
? `${basePath}/oauth/callback?${queryString}`
|
|
117
|
+
: `${basePath}/oauth/callback`;
|
|
118
|
+
res.redirect(302, redirectUrl);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
// Serve the main HTML file for the root inspector path (exact match)
|
|
122
|
+
app.get(basePath, (_req, res) => {
|
|
123
|
+
const indexPath = join(clientDistPath, 'index.html');
|
|
124
|
+
if (!existsSync(indexPath)) {
|
|
125
|
+
res.status(500).send('Inspector UI not found. Please build the inspector package.');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Serve the HTML file (Vite built with base: '/inspector')
|
|
129
|
+
res.sendFile(indexPath);
|
|
130
|
+
});
|
|
131
|
+
// Redirect /inspector/ to /inspector (remove trailing slash)
|
|
132
|
+
app.get(`${basePath}/`, (_req, res) => {
|
|
133
|
+
res.redirect(301, basePath);
|
|
134
|
+
});
|
|
135
|
+
// Serve the main HTML file for all other inspector routes (SPA routing)
|
|
136
|
+
app.get(`${basePath}/*`, (_req, res) => {
|
|
137
|
+
const indexPath = join(clientDistPath, 'index.html');
|
|
138
|
+
if (!existsSync(indexPath)) {
|
|
139
|
+
res.status(500).send('Inspector UI not found. Please build the inspector package.');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Serve the HTML file (Vite built with base: '/inspector')
|
|
143
|
+
res.sendFile(indexPath);
|
|
144
|
+
});
|
|
145
|
+
}
|