Vilnius Transport MCP Server

  • src
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { launchBrowser, type BrowserMethods } from "./browser.js"; import { getSearchPageLinks } from "./extract.js"; import { getReadabilityScript } from "./macro.js"; import { toMarkdown } from "./to-markdown.js"; /** * Interface for search parameters */ interface SearchParams { query: string; excludeDomains?: string[]; limit?: number; truncate?: number; } /** * Interface for search results */ interface SearchResult { title: string; url: string; content?: string; } /** * Get Google search URL with parameters */ function getSearchUrl(options: SearchParams) { const searchParams = new URLSearchParams({ q: `${ options.excludeDomains && options.excludeDomains.length > 0 ? `${options.excludeDomains.map((domain) => `-site:${domain}`).join(" ")} ` : "" }${options.query}`, num: `${options.limit || 10}`, }); // web tab searchParams.set("udm", "14"); return `https://www.google.com/search?${searchParams.toString()}`; } /** * Execute web search using browser */ async function executeWebSearch(params: SearchParams): Promise<SearchResult[]> { const browser = await launchBrowser({ type: "fake" }); const visitedUrls = new Set<string>(); try { const url = getSearchUrl(params); const links = await browser.evaluateOnPage(url, getSearchPageLinks, []); if (!links || links.length === 0) { return []; } const validLinks = links.filter((link) => { if (visitedUrls.has(link.url)) return false; visitedUrls.add(link.url); return true; }); const readabilityScript = await getReadabilityScript(); const results = await Promise.all( validLinks.map((item) => visitLink(browser, item.url, readabilityScript)) ); return results .filter((result): result is SearchResult => result !== null) .map((result) => ({ ...result, content: params.truncate ? result.content?.slice(0, params.truncate) : result.content, })); } finally { await browser.close(); } } /** * Visit a link and extract content */ async function visitLink( browser: BrowserMethods, url: string, readabilityScript: string ): Promise<SearchResult | null> { const result = await browser.evaluateOnPage( url, (window, readabilityScript) => { const Readability = new Function( "module", `${readabilityScript}\nreturn module.exports` )({}); const document = window.document; const selectorsToRemove = [ "script,noscript,style,link,svg,img,video,iframe,canvas", ".reflist", // wikipedia refs ]; document .querySelectorAll(selectorsToRemove.join(",")) .forEach((el) => el.remove()); const article = new Readability(document).parse(); const content = article?.content || ""; const title = document.title; return { content, title: article?.title || title }; }, [readabilityScript] ); if (!result) return null; const content = toMarkdown(result.content); return { ...result, url, content }; } // Define the local web search tool const LOCAL_WEB_SEARCH_TOOL: Tool = { name: "local_web_search", description: "Performs web search and returns results with title, URL and description.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query to find relevant content" }, excludeDomains: { type: "array", items: { type: "string" }, description: "List of domains to exclude from search results", default: [] }, limit: { type: "number", description: "Maximum number of results to return (default: 20)", default: 20 }, truncate: { type: "number", description: "Maximum length of content to return per result" } }, required: ["query"] } }; // Server implementation const server = new Server( { name: "local-web-search", version: "0.1.0", }, { capabilities: { tools: {}, }, } ); function isLocalWebSearchArgs(args: unknown): args is SearchParams { return ( typeof args === "object" && args !== null && "query" in args && typeof (args as { query: string }).query === "string" ); } // Handle tool listing server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [LOCAL_WEB_SEARCH_TOOL], })); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; if (!args) { throw new Error("No arguments provided"); } switch (name) { case "local_web_search": { if (!isLocalWebSearchArgs(args)) { throw new Error("Invalid arguments for local_web_search"); } const results = await executeWebSearch(args); return { content: [ { type: "text", text: JSON.stringify({ results }) } ], isError: false, }; } default: return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true, }; } } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); // Run server async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Local Web Search MCP Server running on stdio"); } runServer().catch((error) => { console.error("Fatal error running server:", error); process.exit(1); });