Vite MCP Server
by ESnark
Verified
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { randomUUID } from 'crypto';
import fs from 'fs/promises';
import path from 'path';
import puppeteer from 'puppeteer';
import { SCREENSHOTS_DIRECTORY } from '../constants.js';
import { Logger } from '../utils/logger.js';
// Screenshot repository
interface Screenshot {
id: string;
timestamp: string;
mimeType: string;
filePath: string; // File system path
description: string;
checkpointId: string | null;
url?: string; // URL where the screenshot was taken
cacheId?: string; // Cache identifier
}
// URL path normalization function (removes protocol, host, and port)
const normalizeUrlPath = (url: string): string => {
try {
const urlObj = new URL(url);
return urlObj.pathname + urlObj.search + urlObj.hash;
} catch (error) {
return url; // Return as is if URL is invalid
}
};
// Create key for URL path and cache ID
const createUrlCacheKey = (urlPath: string, cacheId: string): string => {
return `${urlPath}#${cacheId}`;
};
// Screenshot memory storage
const screenshots: Map<string, Screenshot> = new Map();
// Screenshot ID storage by URL path (stores only the latest screenshot)
const urlPathToScreenshotId: Map<string, string> = new Map();
// Screenshot ID storage by URL path and cache ID
const urlPathCacheIdToScreenshotId: Map<string, string> = new Map();
/**
* Register screenshot resource to MCP server
* @param server MCP server instance
* @param browserRef Browser reference
* @param pageRef Page reference
* @param viteDevServerUrlRef Development server URL reference
*/
export function registerScreenshotResource(
server: McpServer,
browserRef: { current: puppeteer.Browser | null },
pageRef: { current: puppeteer.Page | null },
) {
// Function to check if browser is started
const isBrowserStarted = () => {
return browserRef.current !== null && pageRef.current !== null;
};
// Get checkpoint ID
const getCurrentCheckpointId = async () => {
if (!isBrowserStarted() || !pageRef.current) return null;
try {
const checkpointId = await pageRef.current.evaluate(() => {
const metaTag = document.querySelector('meta[name="__mcp_checkpoint"]');
return metaTag ? metaTag.getAttribute('data-id') : null;
});
return checkpointId;
} catch (error) {
Logger.error(`Failed to get checkpoint ID: ${error}`);
return null;
}
};
// Create screenshot URI
const getScreenshotUri = (id: string) => `screenshot://${id}`;
// Create screenshot URI from URL path
const getScreenshotUriFromPath = (path: string, withCacheId?: boolean) => {
const id = urlPathToScreenshotId.get(path);
if (!id) return null;
// Include cache ID if requested
if (withCacheId) {
const screenshot = screenshots.get(id);
if (screenshot && screenshot.cacheId) {
// Create URL object
try {
const urlObj = new URL(path);
// Add cache-id query parameter
urlObj.searchParams.set('cache-id', screenshot.cacheId);
return urlObj.toString();
} catch (error) {
Logger.error(`Failed to create URL with cache-id: ${error}`);
return getScreenshotUri(id);
}
}
}
return getScreenshotUri(id);
};
// 모든 스크린샷 목록 반환
const getAllScreenshots = () => {
return {
contents: Array.from(screenshots.values()).map(screenshot => ({
uri: getScreenshotUri(screenshot.id),
mimeType: screenshot.mimeType,
blob: '', // Empty blob for list
metadata: {
name: `Screenshot ${screenshot.id}`,
description: screenshot.description,
timestamp: screenshot.timestamp,
checkpointId: screenshot.checkpointId,
url: screenshot.url
}
}))
};
};
// 캐시 ID로 스크린샷 조회
const getScreenshotByCacheId = async (cleanTargetUrl: string, cacheId: string) => {
const normalizedPath = normalizeUrlPath(cleanTargetUrl);
const cacheKey = createUrlCacheKey(normalizedPath, cacheId);
const cachedScreenshotId = urlPathCacheIdToScreenshotId.get(cacheKey);
if (!cachedScreenshotId) {
throw new Error(`Screenshot with cache ID ${cacheId} not found for URL: ${cleanTargetUrl}`);
}
const screenshot = screenshots.get(cachedScreenshotId);
if (!screenshot) {
throw new Error(`Screenshot with ID ${cachedScreenshotId} not found`);
}
// 이미지 데이터 읽어오기
const imageBuffer = await fs.readFile(screenshot.filePath);
return {
contents: [
{
uri: `screenshot://${normalizedPath}?__mcp_cache=${cacheId}`,
mimeType: screenshot.mimeType,
blob: imageBuffer.toString('base64'),
metadata: {
name: `Screenshot ${screenshot.id}`,
description: screenshot.description,
timestamp: screenshot.timestamp,
checkpointId: screenshot.checkpointId,
url: screenshot.url,
cacheId: screenshot.cacheId
}
}
]
};
};
// 새 스크린샷 캡처 및 반환
const captureAndReturnScreenshot = async (cleanTargetUrl: string, uri: URL) => {
if (!isBrowserStarted() || !pageRef.current) {
throw new Error('Browser not started. Cannot capture screenshot.');
}
try {
// 현재 URL 가져오기
const currentUrl = await pageRef.current.url();
// URL이 다르면 이동
if (cleanTargetUrl !== currentUrl) {
Logger.info(`Navigating to ${cleanTargetUrl} before capturing screenshot`);
await pageRef.current.goto(cleanTargetUrl, { waitUntil: 'networkidle0' });
}
// 스크린샷 캡처
const pageScreenshot = await pageRef.current.screenshot({ encoding: 'binary', fullPage: true });
const imageData = pageScreenshot as Buffer;
// 랜덤 ID 생성
const id = randomUUID();
const newCacheId = randomUUID().substring(0, 8);
// 체크포인트 ID 가져오기
const checkpointId = await getCurrentCheckpointId();
// 최종 URL (이동 후 URL이 변경되었을 수 있음)
const finalUrl = await pageRef.current.url();
const urlPath = normalizeUrlPath(finalUrl);
// 파일명 생성 및 저장
const filename = `${id}.png`;
const filePath = path.join(SCREENSHOTS_DIRECTORY, filename);
// 이미지 데이터 파일로 저장
await fs.writeFile(filePath, imageData);
Logger.info(`Screenshot saved to file: ${filePath}`);
// 스크린샷 객체 생성
const screenshot: Screenshot = {
id,
timestamp: new Date().toISOString(),
mimeType: 'image/png',
filePath,
description: `Screenshot of full page at ${finalUrl}`,
checkpointId,
url: finalUrl,
cacheId: newCacheId
};
// 스크린샷 저장
screenshots.set(id, screenshot);
// URL 경로로 최신 스크린샷 ID 저장
urlPathToScreenshotId.set(urlPath, id);
// URL 경로와 캐시 ID 함께 저장
const cacheKey = createUrlCacheKey(urlPath, newCacheId);
urlPathCacheIdToScreenshotId.set(cacheKey, id);
Logger.info(`Screenshot captured with ID: ${id} for URL: ${finalUrl} with cache ID: ${newCacheId}`);
// 이미지 데이터 읽기
const imageBuffer = await fs.readFile(screenshot.filePath);
return {
contents: [
{
uri: uri.href,
mimeType: screenshot.mimeType,
blob: imageBuffer.toString('base64'),
metadata: {
name: `Screenshot ${screenshot.id}`,
description: screenshot.description,
timestamp: screenshot.timestamp,
checkpointId: screenshot.checkpointId,
url: screenshot.url,
cacheId: newCacheId,
cacheUrl: `screenshot://${finalUrl.replace(/^https?:\/\//, '')}?__mcp_cache=${newCacheId}`
}
}
]
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
Logger.error(`Failed to capture screenshot: ${errorMessage}`);
throw new Error(`Failed to capture screenshot: ${errorMessage}`);
}
};
// URL 파싱 및 정규화
const parseScreenshotUrl = (uri: URL) => {
// URL 변환 (screenshot://example.com/... -> http://example.com/...)
const targetUrl = 'http' + uri.href.substring('screenshot'.length);
// 캐시 ID 파라미터 제거
const cleanTargetUrl = targetUrl.replace(/\?__mcp_cache=.*$/, '');
// 캐시 ID 추출
const cacheId = uri.searchParams.get('__mcp_cache');
return { targetUrl, cleanTargetUrl, cacheId };
};
// Expose screenshots as resources (MCP resource API)
server.resource(
'screenshots',
'screenshot://',
async (uri, params: any) => {
// If no specific screenshot is requested, return all screenshots
if (uri.href === 'screenshot://') {
return getAllScreenshots();
}
// 스크린샷 요청 처리 - 모든 URL 지원
if (uri.href.startsWith('screenshot://')) {
const urlObj = new URL(uri.href);
const { cleanTargetUrl, cacheId } = parseScreenshotUrl(urlObj);
// 캐시 ID가 있는 경우 해당 ID로 저장된 스크린샷 조회
if (cacheId) {
return await getScreenshotByCacheId(cleanTargetUrl, cacheId);
}
// 캐시 ID가 없는 경우 새 스크린샷 캡처하고 반환
return await captureAndReturnScreenshot(cleanTargetUrl, urlObj);
}
// 지원하지 않는 URI 형식
throw new Error(`Unsupported screenshot URI format: ${uri.href}`);
}
);
// Find screenshot by URL path
const getScreenshotByPath = (url: string): Screenshot | undefined => {
if (!url) return undefined;
const urlPath = normalizeUrlPath(url);
const screenshotId = urlPathToScreenshotId.get(urlPath);
if (!screenshotId) return undefined;
return screenshots.get(screenshotId);
};
// Return functions to also add screenshots from browser tools
return {
// Function to externally add screenshots
addScreenshot: async (
imageData: string | Buffer,
description: string,
checkpointId: string | null = null,
url?: string
): Promise<string> => {
const id = randomUUID();
const cacheId = randomUUID().substring(0, 8);
// Create filename and save
const filename = `${id}.png`;
const filePath = path.join(SCREENSHOTS_DIRECTORY, filename);
// Save data to file
if (typeof imageData === 'string') {
// Convert base64 string if needed
await fs.writeFile(filePath, Buffer.from(imageData, 'base64'));
} else {
// Save Buffer directly
await fs.writeFile(filePath, imageData);
}
Logger.info(`External screenshot saved to file: ${filePath}`);
const screenshot: Screenshot = {
id,
timestamp: new Date().toISOString(),
mimeType: 'image/png',
filePath,
description,
checkpointId,
url,
cacheId
};
screenshots.set(id, screenshot);
// Map URL path if URL is provided (for all URLs)
if (url) {
const urlPath = normalizeUrlPath(url);
urlPathToScreenshotId.set(urlPath, id);
// Store URL path and cache ID together
const cacheKey = createUrlCacheKey(urlPath, cacheId);
urlPathCacheIdToScreenshotId.set(cacheKey, id);
Logger.info(`Associated URL path ${urlPath} with external screenshot ID: ${id}`);
}
Logger.info(`External screenshot added with ID: ${id}`);
return getScreenshotUri(id);
},
// Find screenshot by URL path
getScreenshotByPath,
// Get screenshot URI from path
getScreenshotUriFromPath
};
}