MCP File Preview Server
by seanivore
Verified
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ListResourceTemplatesRequestSchema,
ReadResourceRequestSchema,
ListPromptsRequestSchema,
Tool,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import * as puppeteer from 'puppeteer';
import * as fs from 'fs';
import * as path from 'path';
// Tool definitions
const previewFileTool: Tool = {
name: 'preview_file',
description: 'Preview local HTML file and capture screenshot',
inputSchema: {
type: 'object',
properties: {
filePath: { type: 'string', description: 'Path to local HTML file' },
width: { type: 'number', description: 'Viewport width', default: 1024 },
height: { type: 'number', description: 'Viewport height', default: 768 }
},
required: ['filePath']
}
};
const analyzeContentTool: Tool = {
name: 'analyze_content',
description: 'Analyze HTML content structure',
inputSchema: {
type: 'object',
properties: {
filePath: { type: 'string', description: 'Path to local HTML file' }
},
required: ['filePath']
}
};
class FilePreviewWrapper {
private browser: puppeteer.Browser | null = null;
async initBrowser() {
if (!this.browser) {
this.browser = await puppeteer.launch();
}
return this.browser;
}
async previewFile(filePath: string, width = 1024, height = 768) {
if (!fs.existsSync(filePath)) {
throw new McpError(ErrorCode.InvalidRequest, `File not found: ${filePath}`);
}
const browser = await this.initBrowser();
const page = await browser.newPage();
await page.setViewport({ width, height });
try {
// Configure browser for better external resource handling
await page.setBypassCSP(true);
await page.setExtraHTTPHeaders({
'Accept-Language': 'en-US,en;q=0.9',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/91.0.4472.114'
});
// Navigate with longer timeout and wait for network idle
await page.goto(`file://${filePath}`, {
waitUntil: ['networkidle0', 'load', 'domcontentloaded'],
timeout: 30000
});
// Read and inject CSS files
const baseDir = path.dirname(filePath);
const mainCss = fs.readFileSync(path.join(baseDir, '..', 'style.css'), 'utf-8');
const pageCss = fs.readFileSync(path.join(baseDir, path.basename(filePath).replace('.html', '.css')), 'utf-8');
await page.addStyleTag({ content: mainCss });
await page.addStyleTag({ content: pageCss });
// Wait for all resources to load
await page.evaluate((): Promise<void[]> => {
return Promise.all(
Array.from(document.images)
.filter(img => !img.complete)
.map(img => new Promise<void>((resolve) => {
img.onload = img.onerror = () => resolve();
}))
);
});
// Small delay for any animations/transitions
await new Promise<void>(resolve => setTimeout(resolve, 1000));
// Create screenshots directory if it doesn't exist
const screenshotsDir = path.join('/Users/seanivore/Projects/mcp-file-preview', 'screenshots');
if (!fs.existsSync(screenshotsDir)) {
fs.mkdirSync(screenshotsDir, { recursive: true });
}
// Generate screenshot filename based on original file
const screenshotName = `${path.basename(filePath, '.html')}_${Date.now()}.png`;
const screenshotPath = path.join(screenshotsDir, screenshotName);
// Take full page screenshot and save to file
await page.screenshot({
fullPage: true,
path: screenshotPath
});
const content = await page.content();
return {
screenshotPath,
content
};
} finally {
await page.close();
}
}
async analyzeContent(filePath: string) {
if (!fs.existsSync(filePath)) {
throw new McpError(ErrorCode.InvalidRequest, `File not found: ${filePath}`);
}
const content = fs.readFileSync(filePath, 'utf-8');
return {
headings: (content.match(/<h[1-6][^>]*>.*?<\/h[1-6]>/g) || []).length,
paragraphs: (content.match(/<p[^>]*>.*?<\/p>/g) || []).length,
images: (content.match(/<img[^>]*>/g) || []).length,
links: (content.match(/<a[^>]*>.*?<\/a>/g) || []).length,
};
}
async cleanup() {
if (this.browser) {
await this.browser.close();
this.browser = null;
}
}
}
async function main() {
const server = new Server(
{
name: "File Preview MCP Server",
version: "1.0.0",
},
{
capabilities: {
resources: {
list: true,
read: true,
listTemplates: true,
listChanged: true,
subscribe: true
},
prompts: {},
tools: {
preview_file: previewFileTool,
analyze_content: analyzeContentTool
}
},
}
);
const filePreview = new FilePreviewWrapper();
// Handle tool execution requests
server.setRequestHandler(
CallToolRequestSchema,
async (request) => {
try {
if (!request.params.arguments) {
throw new Error("No arguments provided");
}
switch (request.params.name) {
case "preview_file": {
const args = request.params.arguments as { filePath: string; width?: number; height?: number };
if (!args.filePath) {
throw new Error("Missing required argument: filePath");
}
const result = await filePreview.previewFile(args.filePath, args.width, args.height);
return {
content: [
{
type: "text",
text: `Screenshot saved to: ${result.screenshotPath}\n\nHTML Content:\n${result.content}`
}
],
};
}
case "analyze_content": {
const args = request.params.arguments as { filePath: string };
if (!args.filePath) {
throw new Error("Missing required argument: filePath");
}
const analysis = await filePreview.analyzeContent(args.filePath);
return {
content: [
{
type: "text",
text: JSON.stringify(analysis, null, 2)
}
],
};
}
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify({
error: error instanceof Error ? error.message : String(error),
}),
},
],
};
}
}
);
// Handle tool listing requests
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [previewFileTool, analyzeContentTool],
};
});
// Handle resource listing requests
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "file://preview/html",
name: "HTML Preview",
description: "Preview HTML files with styling",
mimeType: "text/html"
}
]
};
});
// Handle resource template listing requests
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
return {
resourceTemplates: [
{
uriTemplate: "file://preview/html/{path}",
name: "HTML File Preview",
description: "Preview any HTML file with its associated CSS",
mimeType: "text/html"
}
]
};
});
// Handle resource read requests
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
const match = uri.match(/^file:\/\/preview\/html\/(.+)$/);
if (!match) {
throw new McpError(ErrorCode.InvalidRequest, `Invalid resource URI format: ${uri}`);
}
const filePath = decodeURIComponent(match[1]);
try {
const result = await filePreview.previewFile(filePath);
return {
contents: [
{
uri,
mimeType: "text/html",
text: result.content
}
]
};
} catch (error) {
throw new McpError(
ErrorCode.InvalidRequest,
error instanceof Error ? error.message : String(error)
);
}
});
// Handle prompts listing requests
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: []
};
});
const transport = new StdioServerTransport();
await server.connect(transport);
process.on('SIGINT', async () => {
await filePreview.cleanup();
await server.close();
process.exit(0);
});
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});