Google Jobs MCP Server

  • src
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import fetch from "node-fetch"; import type { Response } from "node-fetch"; // Check API key const SERP_API_KEY = process.env.SERP_API_KEY; if (!SERP_API_KEY) { console.error("Error: SERP_API_KEY environment variable is required"); process.exit(1); } // Define supported languages type SupportedLanguage = "en" | "zh-CN" | "ja" | "ko"; // Language localization const localizations = { "en": { statistics: "Search Statistics", totalFound: "Total Found", currentPage: "Current Page", searchTime: "Search Time", seconds: "seconds", pageHint: "pages of results. Use 'page' parameter to view more", noResults: "No matching jobs found", suggestions: "Suggestions", position: "Position", company: "Company", location: "Location", postTime: "Posted", jobType: "Job Type", salary: "Salary Range", highlights: "Job Highlights", benefits: "Company Benefits", description: "Job Description", applyLink: "Apply Link", unspecified: "Unspecified", noDescription: "No description available", noApplyLink: "No direct apply link", warning: "Warning", error: "Error", searchTips: "Search Tips" }, "zh-CN": { statistics: "搜索结果统计", totalFound: "总计找到", currentPage: "当前页码", searchTime: "搜索用时", seconds: "秒", pageHint: "页结果,可通过设置 page 参数查看更多", noResults: "未找到匹配的职位", suggestions: "建议", position: "职位", company: "公司", location: "地点", postTime: "发布时间", jobType: "工作类型", salary: "薪资范围", highlights: "工作亮点", benefits: "公司福利", description: "职位描述", applyLink: "申请链接", unspecified: "未指定", noDescription: "暂无描述", noApplyLink: "无直接申请链接", warning: "提示", error: "搜索出错", searchTips: "搜索建议" }, "ja": { statistics: "検索統計", totalFound: "検索結果数", currentPage: "現在のページ", searchTime: "検索時間", seconds: "秒", pageHint: "ページの結果があります。pageパラメータで他のページを表示できます", noResults: "該当する求人が見つかりません", suggestions: "提案", position: "職位", company: "会社", location: "勤務地", postTime: "掲載日", jobType: "雇用形態", salary: "給与", highlights: "仕事の特徴", benefits: "福利厚生", description: "職務内容", applyLink: "応募リンク", unspecified: "未指定", noDescription: "説明なし", noApplyLink: "直接応募リンクなし", warning: "警告", error: "エラー", searchTips: "検索のヒント" }, "ko": { statistics: "검색 통계", totalFound: "검색 결과 수", currentPage: "현재 페이지", searchTime: "검색 시간", seconds: "초", pageHint: "페이지의 결과가 있습니다. page 매개변수로 다른 페이지를 볼 수 있습니다", noResults: "일치하는 채용 공고를 찾을 수 없습니다", suggestions: "제안", position: "직위", company: "회사", location: "근무지", postTime: "게시일", jobType: "고용 형태", salary: "급여", highlights: "직무 특징", benefits: "복리후생", description: "직무 설명", applyLink: "지원 링크", unspecified: "미지정", noDescription: "설명 없음", noApplyLink: "직접 지원 링크 없음", warning: "경고", error: "오류", searchTips: "검색 팁" } } as const; // Define search tool const SEARCH_JOBS_TOOL = { name: "search_jobs", description: `Google Jobs API search tool. Supported search parameters: 1. Basic Search: Job title or keywords 2. Location: City or region 3. Time Filter: Recently posted jobs 4. Job Type: Full-time, part-time, contract, internship 5. Salary Range: Filter by compensation 6. Geographic Range: Set search radius 7. Language: Multi-language support All parameters except 'query' are optional and can be freely combined.`, inputSchema: { type: "object", properties: { query: { type: "string", description: "Search keywords (Required, e.g., 'software engineer', 'data analyst', 'product manager')" }, location: { type: "string", description: "Job location (Optional, e.g., 'New York', 'London', 'Tokyo')", default: "" }, posted_age: { type: "string", description: `Posting date filter (Optional) Options: - "today": Posted today - "3days": Last 3 days - "week": Last week - "month": Last month`, default: "" }, employment_type: { type: "string", description: `Job type (Optional) Options: - "FULLTIME": Full-time - "PARTTIME": Part-time - "CONTRACTOR": Contractor - "INTERN": Internship - "TEMPORARY": Temporary`, default: "" }, salary: { type: "string", description: `Salary range (Optional) Format examples: - "$50K+": Above $50,000 - "$100K+": Above $100,000 - "$150K+": Above $150,000`, default: "" }, radius: { type: "string", description: `Search radius (Optional) Format examples: - "10mi": Within 10 miles - "20mi": Within 20 miles - "50mi": Within 50 miles`, default: "" }, hl: { type: "string", description: `Result language (Optional) Options: - "en": English - "zh-CN": Chinese - "ja": Japanese - "ko": Korean`, default: "en" }, page: { type: "number", description: `Page number (Optional, default: 1) - 10 results per page - Supports pagination`, default: 1 }, sort_by: { type: "string", description: `Sort order (Optional) Options: - "date": Sort by date - "relevance": Sort by relevance - "salary": Sort by salary`, default: "relevance" } }, required: ["query"] } }; // 服务器实例 const server = new Server({ name: "google-jobs-search", version: "0.1.0", }, { capabilities: { tools: {} } }); // Get localized text based on language function getLocalizedText(lang: SupportedLanguage = "en") { return localizations[lang] || localizations["en"]; } // Smart error handling with localization function getSearchSuggestions(error: any, query: string, lang: SupportedLanguage = "en"): string { const t = getLocalizedText(lang); let suggestions = `\n💡 ${t.searchTips}:\n`; if (error.message.includes("API error: 429")) { return suggestions + "API rate limit exceeded. Please try again later."; } if (error.message.includes("Invalid location")) { suggestions += `• Location input might be incorrect, try:\n`; suggestions += ` - Using standard location names (e.g., "New York" instead of "NY")\n`; suggestions += ` - Checking spelling\n`; return suggestions; } // Provide suggestions based on query content if (query.length > 100) { suggestions += `• Search query too long, consider shortening\n`; } if (query.includes("&") || query.includes("|")) { suggestions += `• Avoid special characters, use plain text\n`; } suggestions += `• Try using more general job titles\n`; suggestions += `• Reduce filter conditions\n`; suggestions += `• Expand search area or try different location\n`; return suggestions; } // Parameter validation with localization function validateSearchParams(args: any, lang: SupportedLanguage = "en"): string[] { const t = getLocalizedText(lang); const warnings: string[] = []; if (args.location && args.location.length < 2) { warnings.push("Location name too short, please enter complete location name"); } if (args.salary && !args.salary.includes("$") && !args.salary.includes("K")) { warnings.push("Invalid salary format, use format like '$50K+'"); } if (args.radius && !args.radius.endsWith("mi")) { warnings.push("Invalid radius format, use format like '20mi'"); } return warnings; } // Format search results with localization function formatJobResults(jobs: any[], metadata: any, page: number = 1, lang: SupportedLanguage = "en") { const t = getLocalizedText(lang); let output = `📊 ${t.statistics}\n`; output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; output += `🔍 ${t.totalFound}: ${metadata.total_results || t.unspecified}\n`; output += `📄 ${t.currentPage}: ${page}\n`; output += `⏱️ ${t.searchTime}: ${metadata.total_time_taken || t.unspecified} ${t.seconds}\n\n`; if (metadata.total_results > 10) { output += `💡 ${Math.ceil(metadata.total_results/10)} ${t.pageHint}\n\n`; } if (jobs.length === 0) { output += `❌ ${t.noResults}\n`; output += `${t.suggestions}:\n`; output += `1. Try more general keywords\n`; output += `2. Reduce filters\n`; output += `3. Expand search area\n`; return output; } return output + jobs.map((job, index) => { const extensions = job.detected_extensions || {}; const highlights = job.job_highlights || []; const benefits = extensions.benefits || []; let jobOutput = `\n💼 ${t.position} #${index + 1}\n`; jobOutput += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; jobOutput += `📋 ${t.position}: ${job.title}\n`; jobOutput += `🏢 ${t.company}: ${job.company_name}\n`; jobOutput += `📍 ${t.location}: ${job.location || t.unspecified}\n`; jobOutput += `🕒 ${t.postTime}: ${extensions.posted_at || t.unspecified}\n`; jobOutput += `👔 ${t.jobType}: ${extensions.schedule_type || t.unspecified}\n`; if (extensions.salary_range) { jobOutput += `💰 ${t.salary}: ${extensions.salary_range}\n`; } if (highlights.length > 0) { jobOutput += `\n✨ ${t.highlights}:\n`; highlights.forEach((highlight: any) => { jobOutput += `• ${highlight.title}:\n`; highlight.items.forEach((item: string) => { jobOutput += ` - ${item}\n`; }); }); } if (benefits.length > 0) { jobOutput += `\n🎁 ${t.benefits}:\n`; benefits.forEach((benefit: string) => { jobOutput += `• ${benefit}\n`; }); } jobOutput += `\n📝 ${t.description}:\n${job.description || t.noDescription}\n`; if (job.apply_options?.[0]?.link) { jobOutput += `\n🔗 ${t.applyLink}:\n${job.apply_options[0].link}\n`; } jobOutput += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; return jobOutput; }).join('\n'); } // Interface definitions interface JobSearchResponse { jobs_results?: any[]; search_metadata?: { total_results?: number; total_time_taken?: number; }; page?: number; } // Perform job search async function performJobSearch( query: string, location?: string, posted_age?: string, employment_type?: string, salary?: string, radius?: string, hl?: SupportedLanguage ) { // Parameter validation const warnings = validateSearchParams({ query, location, salary, radius }, hl); let output = ""; if (warnings.length > 0) { const t = getLocalizedText(hl); output += `⚠️ ${t.warning}:\n`; warnings.forEach(warning => { output += `• ${warning}\n`; }); output += `\n`; } // Build search URL const url = new URL('https://serpapi.com/search'); url.searchParams.set('engine', 'google_jobs'); url.searchParams.set('q', query); url.searchParams.set('api_key', SERP_API_KEY as string); // Add basic parameters if (location) { url.searchParams.set('location', location); } if (posted_age) { url.searchParams.set('chips', `date_posted:${posted_age}`); } // Add additional parameters if (employment_type) { url.searchParams.append('chips', `employment_type:${employment_type}`); } if (salary) { url.searchParams.append('chips', `salary:${salary}`); } if (radius) { url.searchParams.set('location_distance', radius); } if (hl) { url.searchParams.set('hl', hl); } // Make API request const response = await fetch(url.toString()); if (!response.ok) { throw new Error(`Serp API error: ${response.status} ${response.statusText}`); } // Process response const data = await response.json() as JobSearchResponse; return output + formatJobResults( data.jobs_results || [], data.search_metadata || {}, data.page || 1, hl ); } // Update request handlers server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [SEARCH_JOBS_TOOL] })); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; if (name !== "search_jobs") { throw new Error("Unknown tool"); } if (!args || typeof args.query !== "string") { throw new Error("Invalid arguments for search_jobs"); } const results = await performJobSearch( args.query, args.location as string | undefined, args.posted_age as string | undefined, args.employment_type as string | undefined, args.salary as string | undefined, args.radius as string | undefined, args.hl as SupportedLanguage | undefined ); return { content: [{ type: "text", text: results }], isError: false }; } catch (error) { const args = request.params.arguments as { query?: string; hl?: SupportedLanguage } | undefined; const query = args?.query || ""; const lang = args?.hl || "en"; const t = getLocalizedText(lang); const suggestions = getSearchSuggestions(error, query, lang); return { content: [{ type: "text", text: `❌ ${t.error}:\n${error instanceof Error ? error.message : String(error)}\n${suggestions}` }], isError: true }; } }); // Server startup async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Google Jobs Search MCP Server running on stdio"); } runServer().catch((error) => { console.error("Fatal error running server:", error); process.exit(1); });