/**
* LinkedIn Search Tool
*/
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { getClient, type SearchFilters, type SearchType } from "../api/client.js";
// Input Schema
const InputSchema = z.object({
session_id: z.string().describe("Session ID from linkedin_auth"),
type: z.enum(["PEOPLE", "JOBS", "COMPANIES", "GROUPS"]).describe("What to search for"),
query: z.string().describe("Search keywords"),
limit: z.number().optional().default(10).describe("Max results (default 10, max 50)"),
// PEOPLE filters
location: z.string().optional().describe("Geo URN (103644278=US, 101165590=UK)"),
currentCompany: z.string().optional().describe("Company ID"),
network: z.array(z.enum(["F", "S", "O"])).optional().describe("F=1st, S=2nd, O=3rd+"),
industry: z.string().optional().describe("Industry code (96=IT, 4=Finance)"),
school: z.string().optional().describe("School ID"),
// JOBS filters
company: z.string().optional().describe("Company ID"),
workplaceType: z.array(z.enum(["1", "2", "3"])).optional().describe("1=On-site, 2=Remote, 3=Hybrid"),
experience: z.array(z.enum(["1", "2", "3", "4", "5", "6"])).optional().describe("1=Intern to 6=Exec"),
jobType: z.array(z.enum(["F", "P", "C", "T", "I"])).optional().describe("F=Full, P=Part, C=Contract"),
timePostedRange: z.enum(["r86400", "r604800", "r2592000"]).optional().describe("24h, week, month"),
salaryBucketV2: z.enum(["1", "2", "3", "4", "5"]).optional().describe("1=$40k+ to 5=$120k+"),
easyApply: z.boolean().optional().describe("Easy Apply only"),
// COMPANIES filters
companySize: z.array(z.enum(["A", "B", "C", "D", "E", "F", "G", "H", "I"])).optional().describe("A=1-10 to H=10K+"),
companyHqGeo: z.string().optional().describe("HQ location Geo URN"),
// GROUPS filters
groupMemberCount: z.enum(["1", "101", "1001", "10001"]).optional().describe("Min members"),
}).strict();
// Output Schema
const SearchResultSchema = z.object({
title: z.string().describe("Name or title of the result"),
subtitle: z.string().optional().describe("Secondary info (company, headline, etc)"),
metadata: z.string().optional().describe("Additional metadata"),
url: z.string().describe("LinkedIn URL"),
});
const OutputSchema = z.object({
type: z.enum(["PEOPLE", "JOBS", "COMPANIES", "GROUPS"]).describe("Search type"),
query: z.string().describe("Search query"),
count: z.number().describe("Number of results"),
results: z.array(SearchResultSchema).describe("Search results"),
}).strict();
const DESCRIPTION = `Search LinkedIn for multiple results.
**Types:** PEOPLE, JOBS, COMPANIES, GROUPS
**Examples:**
- Search people: type=PEOPLE, query="software engineer"
- Remote jobs: type=JOBS, query="developer", workplaceType=["2"]
- Large companies: type=COMPANIES, query="tech", companySize=["H"]
- Active groups: type=GROUPS, query="python", groupMemberCount="10001"`;
export function registerSearchTool(server: McpServer): void {
server.registerTool(
"linkedin_search",
{
title: "LinkedIn Search",
description: DESCRIPTION,
inputSchema: InputSchema,
outputSchema: OutputSchema,
annotations: {
title: "LinkedIn Search",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
async (args) => {
try {
const client = getClient(args.session_id);
// Build filters
const filters: SearchFilters = {};
if (args.location) filters.location = args.location;
if (args.currentCompany) filters.currentCompany = args.currentCompany;
if (args.network) filters.network = args.network;
if (args.industry) filters.industry = args.industry;
if (args.school) filters.school = args.school;
if (args.company) filters.company = args.company;
if (args.workplaceType) filters.workplaceType = args.workplaceType;
if (args.experience) filters.experienceLevel = args.experience;
if (args.jobType) filters.jobType = args.jobType;
if (args.timePostedRange) filters.datePosted = args.timePostedRange;
if (args.salaryBucketV2) filters.salary = args.salaryBucketV2;
if (args.easyApply) filters.easyApply = args.easyApply;
if (args.companySize) filters.companySize = args.companySize;
if (args.companyHqGeo) filters.companyHqGeo = args.companyHqGeo;
if (args.groupMemberCount) filters.groupMemberCount = args.groupMemberCount;
const results = await client.search(args.type as SearchType, args.query, args.limit || 10, filters);
if (results.length === 0) {
return {
content: [{ type: "text" as const, text: `No ${args.type} found for "${args.query}"` }],
structuredContent: { type: args.type, query: args.query, count: 0, results: [] },
};
}
const icons: Record<string, string> = { PEOPLE: "👤", JOBS: "💼", COMPANIES: "🏢", GROUPS: "👥" };
let output = `# ${icons[args.type]} ${args.type}: "${args.query}"\n\nFound ${results.length} results\n\n`;
for (const item of results) {
output += `## ${item.title}\n`;
if (item.subtitle) output += `**${item.subtitle}**\n`;
if (item.metadata) output += `${item.metadata}\n`;
output += `🔗 ${item.url}\n\n`;
}
return {
content: [{ type: "text" as const, text: output }],
structuredContent: { type: args.type, query: args.query, count: results.length, results },
};
} catch (error) {
return {
content: [{ type: "text" as const, text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
isError: true,
};
}
}
);
}