mirror of
https://github.com/BrowserMCP/mcp.git
synced 2025-05-04 16:21:43 +03:00
extract mcp server from monorepo
This commit is contained in:
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# Dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
|
||||
# Turbo
|
||||
.turbo
|
||||
|
||||
# Vercel
|
||||
.vercel
|
||||
|
||||
# Build Outputs
|
||||
.next/
|
||||
out/
|
||||
build
|
||||
dist
|
||||
|
||||
# Debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
10
README.md
10
README.md
@@ -1,2 +1,8 @@
|
||||
# mcp
|
||||
Autobrowser MCP is a Model Context Provider (MCP) server that allows AI applications to control your browser
|
||||
# Browser MCP
|
||||
|
||||
MCP server for Browser MCP.
|
||||
|
||||
- Website: https://browsermcp.io
|
||||
- Docs: https://docs.browsermcp.io
|
||||
|
||||
This repo contains all the core MCP code for Browser MCP, but currently cannot be built on its own due to dependencies on utils and types from the monorepo where it's developed.
|
||||
|
||||
40
package.json
Normal file
40
package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@browsermcp/mcp",
|
||||
"version": "0.1.0",
|
||||
"description": "MCP server for browser automation using Browser MCP",
|
||||
"author": "Browser MCP",
|
||||
"homepage": "https://browsermcp.io",
|
||||
"bugs": "https://github.com/browsermcp/mcp/issues",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"mcp-server-browsermcp": "dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsup src/index.ts --format esm && shx chmod +x dist/*.js",
|
||||
"prepare": "npm run build",
|
||||
"watch": "tsup src/index.ts --format esm --watch ",
|
||||
"inspector": "CLIENT_PORT=9001 SERVER_PORT=9002 pnpx @modelcontextprotocol/inspector node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.8.0",
|
||||
"commander": "^13.1.0",
|
||||
"ws": "^8.18.1",
|
||||
"zod": "^3.24.2",
|
||||
"zod-to-json-schema": "^3.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@r2r/messaging": "workspace:*",
|
||||
"@repo/config": "workspace:*",
|
||||
"@repo/messaging": "workspace:*",
|
||||
"@repo/types": "workspace:*",
|
||||
"@repo/utils": "workspace:*",
|
||||
"@types/ws": "^8.18.0",
|
||||
"shx": "^0.3.4",
|
||||
"tsup": "^8.4.0",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
52
src/context.ts
Normal file
52
src/context.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { createSocketMessageSender } from "@r2r/messaging/ws/sender";
|
||||
import { WebSocket } from "ws";
|
||||
|
||||
import { mcpConfig } from "@repo/config/mcp.config";
|
||||
import { MessagePayload, MessageType } from "@repo/messaging/types";
|
||||
import { SocketMessageMap } from "@repo/types/messages/ws";
|
||||
|
||||
const noConnectionMessage = `No connection to browser extension. In order to proceed, you must first connect a tab by clicking the Browser MCP extension icon in the browser toolbar and clicking the 'Connect' button.`;
|
||||
|
||||
export class Context {
|
||||
private _ws: WebSocket | undefined;
|
||||
|
||||
get ws(): WebSocket {
|
||||
if (!this._ws) {
|
||||
throw new Error(noConnectionMessage);
|
||||
}
|
||||
return this._ws;
|
||||
}
|
||||
|
||||
set ws(ws: WebSocket) {
|
||||
this._ws = ws;
|
||||
}
|
||||
|
||||
hasWs(): boolean {
|
||||
return !!this._ws;
|
||||
}
|
||||
|
||||
async sendSocketMessage<T extends MessageType<SocketMessageMap>>(
|
||||
type: T,
|
||||
payload: MessagePayload<SocketMessageMap, T>,
|
||||
options: { timeoutMs?: number } = { timeoutMs: 30000 }
|
||||
) {
|
||||
const { sendSocketMessage } = createSocketMessageSender<SocketMessageMap>(
|
||||
this.ws
|
||||
);
|
||||
try {
|
||||
return await sendSocketMessage(type, payload, options);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message === mcpConfig.errors.noConnectedTab) {
|
||||
throw new Error(noConnectionMessage);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (!this._ws) {
|
||||
return;
|
||||
}
|
||||
await this._ws.close();
|
||||
}
|
||||
}
|
||||
66
src/index.ts
Normal file
66
src/index.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env node
|
||||
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { program } from "commander";
|
||||
|
||||
import { appConfig } from "@repo/config/app.config";
|
||||
|
||||
import type { Resource } from "@/resources/resource";
|
||||
import { createServerWithTools } from "@/server";
|
||||
import * as common from "@/tools/common";
|
||||
import * as custom from "@/tools/custom";
|
||||
import * as snapshot from "@/tools/snapshot";
|
||||
import type { Tool } from "@/tools/tool";
|
||||
|
||||
import packageJSON from "../package.json";
|
||||
|
||||
function setupExitWatchdog(server: Server) {
|
||||
process.stdin.on("close", async () => {
|
||||
setTimeout(() => process.exit(0), 15000);
|
||||
await server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
const commonTools: Tool[] = [common.pressKey, common.wait];
|
||||
|
||||
const customTools: Tool[] = [custom.getConsoleLogs];
|
||||
|
||||
const snapshotTools: Tool[] = [
|
||||
common.navigate(true),
|
||||
common.goBack(true),
|
||||
common.goForward(true),
|
||||
snapshot.snapshot,
|
||||
snapshot.click,
|
||||
snapshot.hover,
|
||||
snapshot.type,
|
||||
snapshot.selectOption,
|
||||
...commonTools,
|
||||
...customTools,
|
||||
];
|
||||
|
||||
const resources: Resource[] = [];
|
||||
|
||||
async function createServer(): Promise<Server> {
|
||||
return createServerWithTools({
|
||||
name: appConfig.name,
|
||||
version: packageJSON.version,
|
||||
tools: snapshotTools,
|
||||
resources,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: Tools must be defined *before* calling `createServer` because only declarations are hoisted, not the initializations
|
||||
*/
|
||||
program
|
||||
.version("Version " + packageJSON.version)
|
||||
.name(packageJSON.name)
|
||||
.action(async () => {
|
||||
const server = await createServer();
|
||||
setupExitWatchdog(server);
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
});
|
||||
program.parse(process.argv);
|
||||
20
src/resources/resource.ts
Normal file
20
src/resources/resource.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { Context } from "../context";
|
||||
|
||||
export type ResourceSchema = {
|
||||
uri: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
mimeType?: string;
|
||||
};
|
||||
|
||||
export type ResourceResult = {
|
||||
uri: string;
|
||||
mimeType?: string;
|
||||
text?: string;
|
||||
blob?: string;
|
||||
};
|
||||
|
||||
export type Resource = {
|
||||
schema: ResourceSchema;
|
||||
read: (context: Context, uri: string) => Promise<ResourceResult[]>;
|
||||
};
|
||||
92
src/server.ts
Normal file
92
src/server.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
ReadResourceRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
import { Context } from "@/context";
|
||||
import type { Resource } from "@/resources/resource";
|
||||
import type { Tool } from "@/tools/tool";
|
||||
import { createWebSocketServer } from "@/ws";
|
||||
|
||||
type Options = {
|
||||
name: string;
|
||||
version: string;
|
||||
tools: Tool[];
|
||||
resources: Resource[];
|
||||
};
|
||||
|
||||
export async function createServerWithTools(options: Options): Promise<Server> {
|
||||
const { name, version, tools, resources } = options;
|
||||
const context = new Context();
|
||||
const server = new Server(
|
||||
{ name, version },
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const wss = await createWebSocketServer();
|
||||
wss.on("connection", (websocket) => {
|
||||
// Close any existing connections
|
||||
if (context.hasWs()) {
|
||||
context.ws.close();
|
||||
}
|
||||
context.ws = websocket;
|
||||
});
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return { tools: tools.map((tool) => tool.schema) };
|
||||
});
|
||||
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||
return { resources: resources.map((resource) => resource.schema) };
|
||||
});
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const tool = tools.find((tool) => tool.schema.name === request.params.name);
|
||||
if (!tool) {
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: `Tool "${request.params.name}" not found` },
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await tool.handle(context, request.params.arguments);
|
||||
return result;
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: "text", text: String(error) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||
const resource = resources.find(
|
||||
(resource) => resource.schema.uri === request.params.uri,
|
||||
);
|
||||
if (!resource) {
|
||||
return { contents: [] };
|
||||
}
|
||||
|
||||
const contents = await resource.read(context, request.params.uri);
|
||||
return { contents };
|
||||
});
|
||||
|
||||
server.close = async () => {
|
||||
await server.close();
|
||||
await wss.close();
|
||||
await context.close();
|
||||
};
|
||||
|
||||
return server;
|
||||
}
|
||||
120
src/tools/common.ts
Normal file
120
src/tools/common.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { zodToJsonSchema } from "zod-to-json-schema";
|
||||
|
||||
import {
|
||||
GoBackTool,
|
||||
GoForwardTool,
|
||||
NavigateTool,
|
||||
PressKeyTool,
|
||||
WaitTool,
|
||||
} from "@repo/types/mcp/tool";
|
||||
|
||||
import { captureAriaSnapshot } from "@/utils/aria-snapshot";
|
||||
|
||||
import type { Tool, ToolFactory } from "./tool";
|
||||
|
||||
export const navigate: ToolFactory = (snapshot) => ({
|
||||
schema: {
|
||||
name: NavigateTool.shape.name.value,
|
||||
description: NavigateTool.shape.description.value,
|
||||
inputSchema: zodToJsonSchema(NavigateTool.shape.arguments),
|
||||
},
|
||||
handle: async (context, params) => {
|
||||
const { url } = NavigateTool.shape.arguments.parse(params);
|
||||
await context.sendSocketMessage("browser_navigate", { url });
|
||||
if (snapshot) {
|
||||
return captureAriaSnapshot(context);
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Navigated to ${url}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const goBack: ToolFactory = (snapshot) => ({
|
||||
schema: {
|
||||
name: GoBackTool.shape.name.value,
|
||||
description: GoBackTool.shape.description.value,
|
||||
inputSchema: zodToJsonSchema(GoBackTool.shape.arguments),
|
||||
},
|
||||
handle: async (context) => {
|
||||
await context.sendSocketMessage("browser_go_back", {});
|
||||
if (snapshot) {
|
||||
return captureAriaSnapshot(context);
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Navigated back",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const goForward: ToolFactory = (snapshot) => ({
|
||||
schema: {
|
||||
name: GoForwardTool.shape.name.value,
|
||||
description: GoForwardTool.shape.description.value,
|
||||
inputSchema: zodToJsonSchema(GoForwardTool.shape.arguments),
|
||||
},
|
||||
handle: async (context) => {
|
||||
await context.sendSocketMessage("browser_go_forward", {});
|
||||
if (snapshot) {
|
||||
return captureAriaSnapshot(context);
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Navigated forward",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const wait: Tool = {
|
||||
schema: {
|
||||
name: WaitTool.shape.name.value,
|
||||
description: WaitTool.shape.description.value,
|
||||
inputSchema: zodToJsonSchema(WaitTool.shape.arguments),
|
||||
},
|
||||
handle: async (context, params) => {
|
||||
const { time } = WaitTool.shape.arguments.parse(params);
|
||||
await context.sendSocketMessage("browser_wait", { time });
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Waited for ${time} seconds`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const pressKey: Tool = {
|
||||
schema: {
|
||||
name: PressKeyTool.shape.name.value,
|
||||
description: PressKeyTool.shape.description.value,
|
||||
inputSchema: zodToJsonSchema(PressKeyTool.shape.arguments),
|
||||
},
|
||||
handle: async (context, params) => {
|
||||
const { key } = PressKeyTool.shape.arguments.parse(params);
|
||||
await context.sendSocketMessage("browser_press_key", { key });
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Pressed key ${key}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
25
src/tools/custom.ts
Normal file
25
src/tools/custom.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { zodToJsonSchema } from "zod-to-json-schema";
|
||||
|
||||
import { GetConsoleLogsTool } from "@repo/types/mcp/tool";
|
||||
|
||||
import { Tool } from "./tool";
|
||||
|
||||
export const getConsoleLogs: Tool = {
|
||||
schema: {
|
||||
name: GetConsoleLogsTool.shape.name.value,
|
||||
description: GetConsoleLogsTool.shape.description.value,
|
||||
inputSchema: zodToJsonSchema(GetConsoleLogsTool.shape.arguments),
|
||||
},
|
||||
handle: async (context, _params) => {
|
||||
const consoleLogs = await context.sendSocketMessage(
|
||||
"browser_get_console_logs",
|
||||
{},
|
||||
);
|
||||
const text: string = consoleLogs
|
||||
.map((log) => JSON.stringify(log))
|
||||
.join("\n");
|
||||
return {
|
||||
content: [{ type: "text", text }],
|
||||
};
|
||||
},
|
||||
};
|
||||
136
src/tools/snapshot.ts
Normal file
136
src/tools/snapshot.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import zodToJsonSchema from "zod-to-json-schema";
|
||||
|
||||
import {
|
||||
ClickTool,
|
||||
DragTool,
|
||||
HoverTool,
|
||||
SelectOptionTool,
|
||||
SnapshotTool,
|
||||
TypeTool,
|
||||
} from "@repo/types/mcp/tool";
|
||||
|
||||
import type { Context } from "@/context";
|
||||
import { captureAriaSnapshot } from "@/utils/aria-snapshot";
|
||||
|
||||
import type { Tool } from "./tool";
|
||||
|
||||
export const snapshot: Tool = {
|
||||
schema: {
|
||||
name: SnapshotTool.shape.name.value,
|
||||
description: SnapshotTool.shape.description.value,
|
||||
inputSchema: zodToJsonSchema(SnapshotTool.shape.arguments),
|
||||
},
|
||||
handle: async (context: Context) => {
|
||||
return await captureAriaSnapshot(context);
|
||||
},
|
||||
};
|
||||
|
||||
export const click: Tool = {
|
||||
schema: {
|
||||
name: ClickTool.shape.name.value,
|
||||
description: ClickTool.shape.description.value,
|
||||
inputSchema: zodToJsonSchema(ClickTool.shape.arguments),
|
||||
},
|
||||
handle: async (context: Context, params) => {
|
||||
const validatedParams = ClickTool.shape.arguments.parse(params);
|
||||
await context.sendSocketMessage("browser_click", validatedParams);
|
||||
const snapshot = await captureAriaSnapshot(context);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Clicked "${validatedParams.element}"`,
|
||||
},
|
||||
...snapshot.content,
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const drag: Tool = {
|
||||
schema: {
|
||||
name: DragTool.shape.name.value,
|
||||
description: DragTool.shape.description.value,
|
||||
inputSchema: zodToJsonSchema(DragTool.shape.arguments),
|
||||
},
|
||||
handle: async (context: Context, params) => {
|
||||
const validatedParams = DragTool.shape.arguments.parse(params);
|
||||
await context.sendSocketMessage("browser_drag", validatedParams);
|
||||
const snapshot = await captureAriaSnapshot(context);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Dragged "${validatedParams.startElement}" to "${validatedParams.endElement}"`,
|
||||
},
|
||||
...snapshot.content,
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const hover: Tool = {
|
||||
schema: {
|
||||
name: HoverTool.shape.name.value,
|
||||
description: HoverTool.shape.description.value,
|
||||
inputSchema: zodToJsonSchema(HoverTool.shape.arguments),
|
||||
},
|
||||
handle: async (context: Context, params) => {
|
||||
const validatedParams = HoverTool.shape.arguments.parse(params);
|
||||
await context.sendSocketMessage("browser_hover", validatedParams);
|
||||
const snapshot = await captureAriaSnapshot(context);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Hovered over "${validatedParams.element}"`,
|
||||
},
|
||||
...snapshot.content,
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const type: Tool = {
|
||||
schema: {
|
||||
name: TypeTool.shape.name.value,
|
||||
description: TypeTool.shape.description.value,
|
||||
inputSchema: zodToJsonSchema(TypeTool.shape.arguments),
|
||||
},
|
||||
handle: async (context: Context, params) => {
|
||||
const validatedParams = TypeTool.shape.arguments.parse(params);
|
||||
await context.sendSocketMessage("browser_type", validatedParams);
|
||||
const snapshot = await captureAriaSnapshot(context);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Typed "${validatedParams.text}" into "${validatedParams.element}"`,
|
||||
},
|
||||
...snapshot.content,
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const selectOption: Tool = {
|
||||
schema: {
|
||||
name: SelectOptionTool.shape.name.value,
|
||||
description: SelectOptionTool.shape.description.value,
|
||||
inputSchema: zodToJsonSchema(SelectOptionTool.shape.arguments),
|
||||
},
|
||||
handle: async (context: Context, params) => {
|
||||
const validatedParams = SelectOptionTool.shape.arguments.parse(params);
|
||||
await context.sendSocketMessage("browser_select_option", validatedParams);
|
||||
const snapshot = await captureAriaSnapshot(context);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Selected option in "${validatedParams.element}"`,
|
||||
},
|
||||
...snapshot.content,
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
28
src/tools/tool.ts
Normal file
28
src/tools/tool.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type {
|
||||
ImageContent,
|
||||
TextContent,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import type { JsonSchema7Type } from "zod-to-json-schema";
|
||||
|
||||
import type { Context } from "@/context";
|
||||
|
||||
export type ToolSchema = {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: JsonSchema7Type;
|
||||
};
|
||||
|
||||
export type ToolResult = {
|
||||
content: (ImageContent | TextContent)[];
|
||||
isError?: boolean;
|
||||
};
|
||||
|
||||
export type Tool = {
|
||||
schema: ToolSchema;
|
||||
handle: (
|
||||
context: Context,
|
||||
params?: Record<string, any>,
|
||||
) => Promise<ToolResult>;
|
||||
};
|
||||
|
||||
export type ToolFactory = (snapshot: boolean) => Tool;
|
||||
26
src/utils/aria-snapshot.ts
Normal file
26
src/utils/aria-snapshot.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Context } from "@/context";
|
||||
import { ToolResult } from "@/tools/tool";
|
||||
|
||||
export async function captureAriaSnapshot(
|
||||
context: Context,
|
||||
status: string = "",
|
||||
): Promise<ToolResult> {
|
||||
const url = await context.sendSocketMessage("getUrl", undefined);
|
||||
const title = await context.sendSocketMessage("getTitle", undefined);
|
||||
const snapshot = await context.sendSocketMessage("browser_snapshot", {});
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `${status ? `${status}\n` : ""}
|
||||
- Page URL: ${url}
|
||||
- Page Title: ${title}
|
||||
- Page Snapshot
|
||||
\`\`\`yaml
|
||||
${snapshot}
|
||||
\`\`\`
|
||||
`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
8
src/utils/log.ts
Normal file
8
src/utils/log.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Logs a message to the console
|
||||
*
|
||||
* `console.error` is used since standard input/output is used as transport for MCP
|
||||
*/
|
||||
export const debugLog: typeof console.error = (...args) => {
|
||||
console.error(...args);
|
||||
};
|
||||
23
src/utils/port.ts
Normal file
23
src/utils/port.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import net from "node:net";
|
||||
|
||||
export async function isPortInUse(port: number): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const server = net.createServer();
|
||||
server.once("error", () => resolve(true)); // Port is still in use
|
||||
server.once("listening", () => {
|
||||
server.close(() => resolve(false)); // Port is free
|
||||
});
|
||||
server.listen(port);
|
||||
});
|
||||
}
|
||||
|
||||
export function killProcessOnPort(port: number) {
|
||||
if (process.platform === "win32") {
|
||||
execSync(
|
||||
`FOR /F "tokens=5" %a in ('netstat -ano ^| findstr :${port}') do taskkill /F /PID %a`,
|
||||
);
|
||||
} else {
|
||||
execSync(`lsof -ti:${port} | xargs kill -9`);
|
||||
}
|
||||
}
|
||||
17
src/ws.ts
Normal file
17
src/ws.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { WebSocketServer } from "ws";
|
||||
|
||||
import { mcpConfig } from "@repo/config/mcp.config";
|
||||
import { wait } from "@repo/utils";
|
||||
|
||||
import { isPortInUse, killProcessOnPort } from "@/utils/port";
|
||||
|
||||
export async function createWebSocketServer(
|
||||
port: number = mcpConfig.defaultWsPort,
|
||||
): Promise<WebSocketServer> {
|
||||
killProcessOnPort(port);
|
||||
// Wait until the port is free
|
||||
while (await isPortInUse(port)) {
|
||||
await wait(100);
|
||||
}
|
||||
return new WebSocketServer({ port });
|
||||
}
|
||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user