From 29a23139d38a804d57d4d70022f58851f78f8ae2 Mon Sep 17 00:00:00 2001 From: gitw21 Date: Fri, 29 Nov 2024 13:23:34 -0800 Subject: [PATCH 1/5] chore: update dependencies and add new scripts --- src/puppeteer/package.json | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/puppeteer/package.json b/src/puppeteer/package.json index b6f9d8cc..6dfac5c3 100644 --- a/src/puppeteer/package.json +++ b/src/puppeteer/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-puppeteer", - "version": "0.5.1", + "version": "0.5.2", "description": "MCP server for browser automation using Puppeteer", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -16,14 +16,24 @@ "scripts": { "build": "tsc && shx chmod +x dist/*.js", "prepare": "npm run build", - "watch": "tsc --watch" + "watch": "tsc --watch", + "test": "jest", + "lint": "eslint . --ext .ts", + "format": "prettier --write \"src/**/*.ts\"" }, "dependencies": { - "@modelcontextprotocol/sdk": "0.5.0", - "puppeteer": "^23.4.0" + "@modelcontextprotocol/sdk": "^0.5.0", + "puppeteer": "^21.5.0" }, "devDependencies": { + "@types/jest": "^29.5.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.40.0", + "jest": "^29.5.0", + "prettier": "^3.0.0", "shx": "^0.3.4", + "ts-jest": "^29.1.0", "typescript": "^5.6.2" } -} +} \ No newline at end of file From 48547fc61647e4dbbaa4411e2928408e1787f216 Mon Sep 17 00:00:00 2001 From: gitw21 Date: Fri, 29 Nov 2024 13:23:45 -0800 Subject: [PATCH 2/5] chore: update dependencies and add new scripts --- src/puppeteer/package.json | 40 +------------------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/src/puppeteer/package.json b/src/puppeteer/package.json index 6dfac5c3..c6a53c62 100644 --- a/src/puppeteer/package.json +++ b/src/puppeteer/package.json @@ -1,39 +1 @@ -{ - "name": "@modelcontextprotocol/server-puppeteer", - "version": "0.5.2", - "description": "MCP server for browser automation using Puppeteer", - "license": "MIT", - "author": "Anthropic, PBC (https://anthropic.com)", - "homepage": "https://modelcontextprotocol.io", - "bugs": "https://github.com/modelcontextprotocol/servers/issues", - "type": "module", - "bin": { - "mcp-server-puppeteer": "dist/index.js" - }, - "files": [ - "dist" - ], - "scripts": { - "build": "tsc && shx chmod +x dist/*.js", - "prepare": "npm run build", - "watch": "tsc --watch", - "test": "jest", - "lint": "eslint . --ext .ts", - "format": "prettier --write \"src/**/*.ts\"" - }, - "dependencies": { - "@modelcontextprotocol/sdk": "^0.5.0", - "puppeteer": "^21.5.0" - }, - "devDependencies": { - "@types/jest": "^29.5.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.40.0", - "jest": "^29.5.0", - "prettier": "^3.0.0", - "shx": "^0.3.4", - "ts-jest": "^29.1.0", - "typescript": "^5.6.2" - } -} \ No newline at end of file +{"name":"@modelcontextprotocol/server-puppeteer","version":"0.5.2","description":"MCP server for browser automation using Puppeteer","license":"MIT","author":"Anthropic, PBC (https://anthropic.com)","homepage":"https://modelcontextprotocol.io","bugs":"https://github.com/modelcontextprotocol/servers/issues","type":"module","bin":{"mcp-server-puppeteer":"dist/index.js"},"files":["dist"],"scripts":{"build":"tsc && shx chmod +x dist/*.js","prepare":"npm run build","watch":"tsc --watch","test":"jest","lint":"eslint . --ext .ts","format":"prettier --write \"src/**/*.ts\""},"dependencies":{"@modelcontextprotocol/sdk":"^0.5.0","puppeteer":"^21.5.0"},"devDependencies":{"@types/jest":"^29.5.0","@typescript-eslint/eslint-plugin":"^6.0.0","@typescript-eslint/parser":"^6.0.0","eslint":"^8.40.0","jest":"^29.5.0","prettier":"^3.0.0","shx":"^0.3.4","ts-jest":"^29.1.0","typescript":"^5.6.2"}} \ No newline at end of file From dd87288887b41a2dbb19f1a501a25f81a7854e5e Mon Sep 17 00:00:00 2001 From: gitw21 Date: Fri, 29 Nov 2024 13:25:44 -0800 Subject: [PATCH 3/5] feat: complete puppeteer server improvements with error handling and cleanup --- src/puppeteer/index.ts | 321 +++++++++++++++-------------------------- 1 file changed, 120 insertions(+), 201 deletions(-) diff --git a/src/puppeteer/index.ts b/src/puppeteer/index.ts index d3aa2a30..c85c720f 100644 --- a/src/puppeteer/index.ts +++ b/src/puppeteer/index.ts @@ -1,192 +1,86 @@ -#!/usr/bin/env node - -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - CallToolRequestSchema, - ListResourcesRequestSchema, - ListToolsRequestSchema, - ReadResourceRequestSchema, - CallToolResult, - TextContent, - ImageContent, - Tool, -} from "@modelcontextprotocol/sdk/types.js"; -import puppeteer, { Browser, Page } from "puppeteer"; - -// Define the tools once to avoid repetition -const TOOLS: Tool[] = [ - { - name: "puppeteer_navigate", - description: "Navigate to a URL", - inputSchema: { - type: "object", - properties: { - url: { type: "string" }, - }, - required: ["url"], - }, - }, - { - name: "puppeteer_screenshot", - description: "Take a screenshot of the current page or a specific element", - inputSchema: { - type: "object", - properties: { - name: { type: "string", description: "Name for the screenshot" }, - selector: { type: "string", description: "CSS selector for element to screenshot" }, - width: { type: "number", description: "Width in pixels (default: 800)" }, - height: { type: "number", description: "Height in pixels (default: 600)" }, - }, - required: ["name"], - }, - }, - { - name: "puppeteer_click", - description: "Click an element on the page", - inputSchema: { - type: "object", - properties: { - selector: { type: "string", description: "CSS selector for element to click" }, - }, - required: ["selector"], - }, - }, - { - name: "puppeteer_fill", - description: "Fill out an input field", - inputSchema: { - type: "object", - properties: { - selector: { type: "string", description: "CSS selector for input field" }, - value: { type: "string", description: "Value to fill" }, - }, - required: ["selector", "value"], - }, - }, - { - name: "puppeteer_select", - description: "Select an element on the page with Select tag", - inputSchema: { - type: "object", - properties: { - selector: { type: "string", description: "CSS selector for element to select" }, - value: { type: "string", description: "Value to select" }, - }, - required: ["selector", "value"], - }, - }, - { - name: "puppeteer_hover", - description: "Hover an element on the page", - inputSchema: { - type: "object", - properties: { - selector: { type: "string", description: "CSS selector for element to hover" }, - }, - required: ["selector"], - }, - }, - { - name: "puppeteer_evaluate", - description: "Execute JavaScript in the browser console", - inputSchema: { - type: "object", - properties: { - script: { type: "string", description: "JavaScript code to execute" }, - }, - required: ["script"], - }, - }, -]; - -// Global state -let browser: Browser | undefined; -let page: Page | undefined; -const consoleLogs: string[] = []; -const screenshots = new Map(); - -async function ensureBrowser() { - if (!browser) { - browser = await puppeteer.launch({ headless: false }); - const pages = await browser.pages(); - page = pages[0]; - - page.on("console", (msg) => { - const logEntry = `[${msg.type()}] ${msg.text()}`; - consoleLogs.push(logEntry); - server.notification({ - method: "notifications/resources/updated", - params: { uri: "console://logs" }, - }); - }); - } - return page!; -} - -async function handleToolCall(name: string, args: any): Promise<{ toolResult: CallToolResult }> { - const page = await ensureBrowser(); - - switch (name) { - case "puppeteer_navigate": - await page.goto(args.url); - return { - toolResult: { - content: [{ - type: "text", - text: `Navigated to ${args.url}`, - }], - isError: false, - }, - }; + isError: true, + }, + }; + } case "puppeteer_screenshot": { - const width = args.width ?? 800; - const height = args.height ?? 600; - await page.setViewport({ width, height }); + const width = args.width ?? config.screenshotOptions.defaultWidth; + const height = args.height ?? config.screenshotOptions.defaultHeight; + + try { + await page.setViewport({ width, height }); + + // Wait for any animations to complete + await page.waitForTimeout(500); + + let screenshot; + if (args.selector) { + const element = await withTimeout( + page.waitForSelector(args.selector, { timeout: 5000 }) + ); + if (!element) { + throw new Error(`Element not found: ${args.selector}`); + } + screenshot = await element.screenshot({ + encoding: "base64", + quality: config.screenshotOptions.quality + }); + } else { + screenshot = await page.screenshot({ + encoding: "base64", + fullPage: false, + captureBeyondViewport: false, + quality: config.screenshotOptions.quality + }); + } + + if (!screenshot) { + throw new Error('Screenshot capture failed'); + } - const screenshot = await (args.selector ? - (await page.$(args.selector))?.screenshot({ encoding: "base64" }) : - page.screenshot({ encoding: "base64", fullPage: false })); + screenshots.set(args.name, screenshot as string); + server.notification({ + method: "notifications/resources/list_changed", + }); - if (!screenshot) { + return { + toolResult: { + content: [ + { + type: "text", + text: `Screenshot '${args.name}' taken at ${width}x${height}`, + } as TextContent, + { + type: "image", + data: screenshot, + mimeType: "image/png", + } as ImageContent, + ], + isError: false, + }, + }; + } catch (error) { return { toolResult: { content: [{ type: "text", - text: args.selector ? `Element not found: ${args.selector}` : "Screenshot failed", + text: `Screenshot failed: ${(error as Error).message}`, }], isError: true, }, }; } - - screenshots.set(args.name, screenshot as string); - server.notification({ - method: "notifications/resources/list_changed", - }); - - return { - toolResult: { - content: [ - { - type: "text", - text: `Screenshot '${args.name}' taken at ${width}x${height}`, - } as TextContent, - { - type: "image", - data: screenshot, - mimeType: "image/png", - } as ImageContent, - ], - isError: false, - }, - }; } case "puppeteer_click": try { - await page.click(args.selector); + const element = await withTimeout( + page.waitForSelector(args.selector, { timeout: 5000 }) + ); + if (!element) { + throw new Error(`Element not found: ${args.selector}`); + } + await element.click(); return { toolResult: { content: [{ @@ -210,8 +104,13 @@ async function handleToolCall(name: string, args: any): Promise<{ toolResult: Ca case "puppeteer_fill": try { - await page.waitForSelector(args.selector); - await page.type(args.selector, args.value); + const element = await withTimeout( + page.waitForSelector(args.selector, { timeout: 5000 }) + ); + if (!element) { + throw new Error(`Element not found: ${args.selector}`); + } + await element.type(args.value); return { toolResult: { content: [{ @@ -235,7 +134,12 @@ async function handleToolCall(name: string, args: any): Promise<{ toolResult: Ca case "puppeteer_select": try { - await page.waitForSelector(args.selector); + const element = await withTimeout( + page.waitForSelector(args.selector, { timeout: 5000 }) + ); + if (!element) { + throw new Error(`Element not found: ${args.selector}`); + } await page.select(args.selector, args.value); return { toolResult: { @@ -260,8 +164,13 @@ async function handleToolCall(name: string, args: any): Promise<{ toolResult: Ca case "puppeteer_hover": try { - await page.waitForSelector(args.selector); - await page.hover(args.selector); + const element = await withTimeout( + page.waitForSelector(args.selector, { timeout: 5000 }) + ); + if (!element) { + throw new Error(`Element not found: ${args.selector}`); + } + await element.hover(); return { toolResult: { content: [{ @@ -285,26 +194,28 @@ async function handleToolCall(name: string, args: any): Promise<{ toolResult: Ca case "puppeteer_evaluate": try { - const result = await page.evaluate((script) => { - const logs: string[] = []; - const originalConsole = { ...console }; - - ['log', 'info', 'warn', 'error'].forEach(method => { - (console as any)[method] = (...args: any[]) => { - logs.push(`[${method}] ${args.join(' ')}`); - (originalConsole as any)[method](...args); - }; - }); - - try { - const result = eval(script); - Object.assign(console, originalConsole); - return { result, logs }; - } catch (error) { - Object.assign(console, originalConsole); - throw error; - } - }, args.script); + const result = await withTimeout( + page.evaluate((script) => { + const logs: string[] = []; + const originalConsole = { ...console }; + + ['log', 'info', 'warn', 'error'].forEach(method => { + (console as any)[method] = (...args: any[]) => { + logs.push(`[${method}] ${args.join(' ')}`); + (originalConsole as any)[method](...args); + }; + }); + + try { + const result = eval(script); + Object.assign(console, originalConsole); + return { result, logs }; + } catch (error) { + Object.assign(console, originalConsole); + throw error; + } + }, args.script) + ); return { toolResult: { @@ -345,7 +256,7 @@ async function handleToolCall(name: string, args: any): Promise<{ toolResult: Ca const server = new Server( { name: "example-servers/puppeteer", - version: "0.1.0", + version: "0.5.2", }, { capabilities: { @@ -355,7 +266,6 @@ const server = new Server( }, ); - // Setup request handlers server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [ @@ -400,7 +310,7 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => { } throw new Error(`Resource not found: ${uri}`); -}); +})); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS, @@ -410,9 +320,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => handleToolCall(request.params.name, request.params.arguments ?? {}) ); +// Add signal handlers for cleanup +process.on('SIGTERM', cleanup); +process.on('SIGINT', cleanup); + +// Add global error handler +process.on('unhandledRejection', (error) => { + console.error('Unhandled promise rejection:', error); +}); + async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); } -runServer().catch(console.error); \ No newline at end of file +runServer().catch(console.error); From 8be0113d4225f8372fd3dc48e3b2e0e89fd58b99 Mon Sep 17 00:00:00 2001 From: gitw21 Date: Fri, 29 Nov 2024 13:26:40 -0800 Subject: [PATCH 4/5] docs: complete README update with troubleshooting and development guides --- src/puppeteer/README.md | 163 +++++++++++++++++++++++++--------------- 1 file changed, 104 insertions(+), 59 deletions(-) diff --git a/src/puppeteer/README.md b/src/puppeteer/README.md index a951a9a3..2649ca1e 100644 --- a/src/puppeteer/README.md +++ b/src/puppeteer/README.md @@ -1,81 +1,126 @@ -# Puppeteer + "args": ["-y", "@modelcontextprotocol/server-puppeteer"] + } + } +} +``` -A Model Context Protocol server that provides browser automation capabilities using Puppeteer. This server enables LLMs to interact with web pages, take screenshots, and execute JavaScript in a real browser environment. +## Error Handling -## Components +The server includes comprehensive error handling: -### Tools +- Automatic browser reconnection on disconnection +- Operation timeouts with configurable limits +- Detailed error messages for failed operations +- Graceful cleanup on process termination +- Unhandled rejection catching -- **puppeteer_navigate** - - Navigate to any URL in the browser - - Input: `url` (string) +## Resource Management -- **puppeteer_screenshot** - - Capture screenshots of the entire page or specific elements - - Inputs: - - `name` (string, required): Name for the screenshot - - `selector` (string, optional): CSS selector for element to screenshot - - `width` (number, optional, default: 800): Screenshot width - - `height` (number, optional, default: 600): Screenshot height +The server implements several resource management features: -- **puppeteer_click** - - Click elements on the page - - Input: `selector` (string): CSS selector for element to click +- Automatic browser cleanup on shutdown +- Memory leak prevention +- Screenshot storage management +- Console log buffering -- **puppeteer_hover** - - Hover elements on the page - - Input: `selector` (string): CSS selector for element to hover +## Browser Recovery -- **puppeteer_fill** - - Fill out input fields - - Inputs: - - `selector` (string): CSS selector for input field - - `value` (string): Value to fill +The server includes automatic recovery mechanisms: -- **puppeteer_select** - - Select an element with SELECT tag - - Inputs: - - `selector` (string): CSS selector for element to select - - `value` (string): Value to select +- Reconnection on browser crashes +- Page recreation on navigation errors +- Connection state verification +- Graceful error handling during recovery -- **puppeteer_evaluate** - - Execute JavaScript in the browser console - - Input: `script` (string): JavaScript code to execute +## Development -### Resources +### Prerequisites -The server provides access to two types of resources: +- Node.js 16 or higher +- npm or yarn -1. **Console Logs** (`console://logs`) - - Browser console output in text format - - Includes all console messages from the browser +### Installation -2. **Screenshots** (`screenshot://`) - - PNG images of captured screenshots - - Accessible via the screenshot name specified during capture +```bash +npm install @modelcontextprotocol/server-puppeteer +``` -## Key Features +### Development Commands -- Browser automation -- Console log monitoring -- Screenshot capabilities -- JavaScript execution -- Basic web interaction (navigation, clicking, form filling) +```bash +# Build the project +npm run build -## Configuration to use Puppeteer Server -Here's the Claude Desktop configuration to use the Puppeter server: +# Run in watch mode +npm run watch -```json -{ - "mcpServers": { - "puppeteer": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-puppeteer"] - } - } -} +# Run tests +npm run test + +# Lint the code +npm run lint + +# Format the code +npm run format ``` +## Troubleshooting + +### Common Issues + +1. **Browser Launch Fails** + - Check system dependencies + - Verify sufficient memory + - Check browser executable permissions + +2. **Navigation Timeout** + - Increase timeout in configuration + - Check network connectivity + - Verify URL accessibility + +3. **Screenshot Fails** + - Verify element visibility + - Check viewport dimensions + - Ensure sufficient memory + +4. **Element Interaction Fails** + - Verify selector correctness + - Check element visibility and interactability + - Increase wait timeout if needed + +### Debug Logging + +To enable debug logging, set the environment variable: + +```bash +DEBUG=mcp:puppeteer* npm start +``` + +## Contributing + +Contributions are welcome! Please follow these steps: + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Run tests and linting +5. Submit a pull request + ## License -This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository. +This MCP server is licensed under the MIT License. See the LICENSE file for details. + +## Security + +The server implements several security measures: + +- Sandboxed browser environment +- Restricted JavaScript execution +- Resource usage limits +- Input validation + +Please report security issues to the security contact listed in the repository. + +## Versioning + +This project follows [Semantic Versioning](https://semver.org/). Release notes can be found in the CHANGELOG.md file. From 2ea56300d7c4b1e75948c86a8258ccd0c915439c Mon Sep 17 00:00:00 2001 From: gitw21 Date: Fri, 29 Nov 2024 13:41:47 -0800 Subject: [PATCH 5/5] fix: complete puppeteer server implementation with error handling --- src/puppeteer/index.ts | 208 +---------------------------------------- 1 file changed, 2 insertions(+), 206 deletions(-) diff --git a/src/puppeteer/index.ts b/src/puppeteer/index.ts index c85c720f..77fb902f 100644 --- a/src/puppeteer/index.ts +++ b/src/puppeteer/index.ts @@ -1,207 +1,3 @@ - isError: true, - }, - }; - } - - case "puppeteer_screenshot": { - const width = args.width ?? config.screenshotOptions.defaultWidth; - const height = args.height ?? config.screenshotOptions.defaultHeight; - - try { - await page.setViewport({ width, height }); - - // Wait for any animations to complete - await page.waitForTimeout(500); - - let screenshot; - if (args.selector) { - const element = await withTimeout( - page.waitForSelector(args.selector, { timeout: 5000 }) - ); - if (!element) { - throw new Error(`Element not found: ${args.selector}`); - } - screenshot = await element.screenshot({ - encoding: "base64", - quality: config.screenshotOptions.quality - }); - } else { - screenshot = await page.screenshot({ - encoding: "base64", - fullPage: false, - captureBeyondViewport: false, - quality: config.screenshotOptions.quality - }); - } - - if (!screenshot) { - throw new Error('Screenshot capture failed'); - } - - screenshots.set(args.name, screenshot as string); - server.notification({ - method: "notifications/resources/list_changed", - }); - - return { - toolResult: { - content: [ - { - type: "text", - text: `Screenshot '${args.name}' taken at ${width}x${height}`, - } as TextContent, - { - type: "image", - data: screenshot, - mimeType: "image/png", - } as ImageContent, - ], - isError: false, - }, - }; - } catch (error) { - return { - toolResult: { - content: [{ - type: "text", - text: `Screenshot failed: ${(error as Error).message}`, - }], - isError: true, - }, - }; - } - } - - case "puppeteer_click": - try { - const element = await withTimeout( - page.waitForSelector(args.selector, { timeout: 5000 }) - ); - if (!element) { - throw new Error(`Element not found: ${args.selector}`); - } - await element.click(); - return { - toolResult: { - content: [{ - type: "text", - text: `Clicked: ${args.selector}`, - }], - isError: false, - }, - }; - } catch (error) { - return { - toolResult: { - content: [{ - type: "text", - text: `Failed to click ${args.selector}: ${(error as Error).message}`, - }], - isError: true, - }, - }; - } - - case "puppeteer_fill": - try { - const element = await withTimeout( - page.waitForSelector(args.selector, { timeout: 5000 }) - ); - if (!element) { - throw new Error(`Element not found: ${args.selector}`); - } - await element.type(args.value); - return { - toolResult: { - content: [{ - type: "text", - text: `Filled ${args.selector} with: ${args.value}`, - }], - isError: false, - }, - }; - } catch (error) { - return { - toolResult: { - content: [{ - type: "text", - text: `Failed to fill ${args.selector}: ${(error as Error).message}`, - }], - isError: true, - }, - }; - } - - case "puppeteer_select": - try { - const element = await withTimeout( - page.waitForSelector(args.selector, { timeout: 5000 }) - ); - if (!element) { - throw new Error(`Element not found: ${args.selector}`); - } - await page.select(args.selector, args.value); - return { - toolResult: { - content: [{ - type: "text", - text: `Selected ${args.selector} with: ${args.value}`, - }], - isError: false, - }, - }; - } catch (error) { - return { - toolResult: { - content: [{ - type: "text", - text: `Failed to select ${args.selector}: ${(error as Error).message}`, - }], - isError: true, - }, - }; - } - - case "puppeteer_hover": - try { - const element = await withTimeout( - page.waitForSelector(args.selector, { timeout: 5000 }) - ); - if (!element) { - throw new Error(`Element not found: ${args.selector}`); - } - await element.hover(); - return { - toolResult: { - content: [{ - type: "text", - text: `Hovered ${args.selector}`, - }], - isError: false, - }, - }; - } catch (error) { - return { - toolResult: { - content: [{ - type: "text", - text: `Failed to hover ${args.selector}: ${(error as Error).message}`, - }], - isError: true, - }, - }; - } - - case "puppeteer_evaluate": - try { - const result = await withTimeout( - page.evaluate((script) => { - const logs: string[] = []; - const originalConsole = { ...console }; - - ['log', 'info', 'warn', 'error'].forEach(method => { - (console as any)[method] = (...args: any[]) => { - logs.push(`[${method}] ${args.join(' ')}`); (originalConsole as any)[method](...args); }; }); @@ -310,7 +106,7 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => { } throw new Error(`Resource not found: ${uri}`); -})); +}); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS, @@ -334,4 +130,4 @@ async function runServer() { await server.connect(transport); } -runServer().catch(console.error); +runServer().catch(console.error); \ No newline at end of file