HDW MCP Server

MIT License
4
  • Apple
  • src
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError, Tool } from "@modelcontextprotocol/sdk/types.js"; import dotenv from "dotenv"; import https from "https"; import { Buffer } from "buffer"; import { LinkedinSearchUsersArgs, LinkedinUserProfileArgs, LinkedinEmailUserArgs, LinkedinUserPostsArgs, LinkedinUserReactionsArgs, LinkedinChatMessagesArgs, SendLinkedinChatMessageArgs, SendLinkedinConnectionArgs, SendLinkedinPostCommentArgs, GetLinkedinUserConnectionsArgs, GetLinkedinPostRepostsArgs, GetLinkedinPostCommentsArgs, GetLinkedinGoogleCompanyArgs, GetLinkedinCompanyArgs, GetLinkedinCompanyEmployeesArgs, SendLinkedinPostArgs, isValidLinkedinSearchUsersArgs, isValidLinkedinUserProfileArgs, isValidLinkedinEmailUserArgs, isValidLinkedinUserPostsArgs, isValidLinkedinUserReactionsArgs, isValidLinkedinChatMessagesArgs, isValidSendLinkedinChatMessageArgs, isValidSendLinkedinConnectionArgs, isValidSendLinkedinPostCommentArgs, isValidGetLinkedinUserConnectionsArgs, isValidGetLinkedinPostRepostsArgs, isValidGetLinkedinPostCommentsArgs, isValidGetLinkedinGoogleCompanyArgs, isValidGetLinkedinCompanyArgs, isValidGetLinkedinCompanyEmployeesArgs, isValidSendLinkedinPostArgs } from "./types.js"; dotenv.config(); const API_KEY = process.env.HDW_ACCESS_TOKEN; const ACCOUNT_ID = process.env.HDW_ACCOUNT_ID; if (!API_KEY) { throw new Error("HDW_ACCESS_TOKEN environment variable is required"); } if (!ACCOUNT_ID) { throw new Error("HDW_ACCOUNT_ID environment variable is required for chat endpoints"); } const API_CONFIG = { BASE_URL: "https://api.horizondatawave.ai", DEFAULT_QUERY: "software engineer", ENDPOINTS: { SEARCH_USERS: "/api/linkedin/search/users", USER_PROFILE: "/api/linkedin/user", USER_EXPERIENCE: "/api/linkedin/user/experience", USER_EDUCATION: "/api/linkedin/user/education", USER_SKILLS: "/api/linkedin/user/skills", LINKEDIN_EMAIL: "/api/linkedin/email/user", LINKEDIN_USER_POSTS: "/api/linkedin/user/posts", LINKEDIN_USER_REACTIONS: "/api/linkedin/user/reactions", CHAT_MESSAGES: "/api/linkedin/management/chat/messages", CHAT_MESSAGE: "/api/linkedin/management/chat/message", USER_CONNECTION: "/api/linkedin/management/user/connection", USER_CONNECTIONS: "/api/linkedin/management/user/connections", POST_COMMENT: "/api/linkedin/management/post/comment", LINKEDIN_POST: "/api/linkedin/management/post", LINKEDIN_POST_REPOSTS: "/api/linkedin/post/reposts", LINKEDIN_POST_COMMENTS: "/api/linkedin/post/comments", LINKEDIN_GOOGLE_COMPANY: "/api/linkedin/google/company", LINKEDIN_COMPANY: "/api/linkedin/company", LINKEDIN_COMPANY_EMPLOYEES: "/api/linkedin/company/employees", } } as const; function formatError(error: unknown): string { if (error instanceof Error) return error.message; return String(error); } function log(message: string, ...args: any[]) { console.error(`[${new Date().toISOString()}] ${message}`, ...args); } async function makeGetRequestWithBody(url: string, data: any): Promise<any> { return new Promise((resolve, reject) => { const bodyString = JSON.stringify(data); const options = { method: "GET", headers: { "Content-Type": "application/json", "access-token": API_KEY!, "Content-Length": Buffer.byteLength(bodyString, "utf-8").toString() } }; log(`Making GET request to ${url} with body: ${bodyString}`); const req = https.request(url, options, (res) => { let rawData = ""; res.on("data", (chunk) => { rawData += chunk; }); res.on("end", () => { try { const parsedData = JSON.parse(rawData); resolve(parsedData); } catch (e) { reject(e); } }); }); req.on("error", (e) => reject(e)); req.write(bodyString); req.end(); }); } async function makeRequest(endpoint: string, data: any, method: string = "POST"): Promise<any> { if (method === "GET" && (endpoint === API_CONFIG.ENDPOINTS.CHAT_MESSAGES || endpoint === API_CONFIG.ENDPOINTS.USER_CONNECTIONS)) { const url = API_CONFIG.BASE_URL.replace(/\/+$/, "") + endpoint; return await makeGetRequestWithBody(url, data); } else { const baseUrl = API_CONFIG.BASE_URL.replace(/\/+$/, ""); const url = baseUrl + (endpoint.startsWith("/") ? endpoint : `/${endpoint}`); const headers = new Headers(); headers.append("Content-Type", "application/json"); headers.append("access-token", API_KEY!); const options: RequestInit = { method, headers, body: JSON.stringify(data) }; log(`Making ${method} request to ${endpoint} with data: ${JSON.stringify(data)}`); const startTime = Date.now(); try { const response = await fetch(url, options); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(`API error: ${response.status} ${errorData.message || response.statusText}`); } const result = await response.json(); log(`API request to ${endpoint} completed in ${Date.now() - startTime}ms`); return result; } catch (error) { log(`API request to ${endpoint} failed after ${Date.now() - startTime}ms:`, error); throw error; } } } function normalizeUserURN(urn: string): string { if (!urn.includes(":")) { log(`Warning: URN format might be missing a prefix. Adding "fsd_profile:" to: ${urn}`); return `fsd_profile:${urn}`; } return urn; } function isValidUserURN(urn: string): boolean { return urn.startsWith("fsd_profile:"); } const SEARCH_LINKEDIN_USERS_TOOL: Tool = { name: "search_linkedin_users", description: "Search for LinkedIn users with various filters like keywords, name, title, company, location etc.", inputSchema: { type: "object", properties: { keywords: { type: "string", description: "Any keyword for searching in the user page." }, first_name: { type: "string", description: "Exact first name" }, last_name: { type: "string", description: "Exact last name" }, title: { type: "string", description: "Exact word in the title" }, company_keywords: { type: "string", description: "Exact word in the company name" }, school_keywords: { type: "string", description: "Exact word in the school name" }, current_company: { type: "string", description: "Company URN or name" }, past_company: { type: "string", description: "Past company URN or name" }, location: { type: "string", description: "Location name or URN" }, industry: { type: "string", description: "Industry URN or name" }, education: { type: "string", description: "Education URN or name" }, count: { type: "number", description: "Maximum number of results (max 1000)", default: 10 }, timeout: { type: "number", description: "Timeout in seconds (20-1500)", default: 300 } }, required: ["count"] } }; const GET_LINKEDIN_PROFILE_TOOL: Tool = { name: "get_linkedin_profile", description: "Get detailed information about a LinkedIn user profile", inputSchema: { type: "object", properties: { user: { type: "string", description: "User alias, URL, or URN" }, with_experience: { type: "boolean", description: "Include experience info", default: true }, with_education: { type: "boolean", description: "Include education info", default: true }, with_skills: { type: "boolean", description: "Include skills info", default: true } }, required: ["user"] } }; const GET_LINKEDIN_EMAIL_TOOL: Tool = { name: "get_linkedin_email_user", description: "Get LinkedIn user details by email", inputSchema: { type: "object", properties: { email: { type: "string", description: "Email address" }, count: { type: "number", description: "Max results", default: 5 }, timeout: { type: "number", description: "Timeout in seconds", default: 300 } }, required: ["email"] } }; const GET_LINKEDIN_USER_POSTS_TOOL: Tool = { name: "get_linkedin_user_posts", description: "Get LinkedIn posts for a user by URN (must include prefix, example: fsd_profile:ACoAAEWn01QBWENVMWqyM3BHfa1A-xsvxjdaXsY)", inputSchema: { type: "object", properties: { urn: { type: "string", description: "User URN (must include prefix, example: fsd_profile:ACoAA...)" }, count: { type: "number", description: "Max posts", default: 10 }, timeout: { type: "number", description: "Timeout in seconds", default: 300 } }, required: ["urn"] } }; const GET_LINKEDIN_USER_REACTIONS_TOOL: Tool = { name: "get_linkedin_user_reactions", description: "Get LinkedIn reactions for a user by URN (must include prefix, example: fsd_profile:ACoAA...)", inputSchema: { type: "object", properties: { urn: { type: "string", description: "User URN (must include prefix, example: fsd_profile:ACoAA...)" }, count: { type: "number", description: "Max reactions", default: 10 }, timeout: { type: "number", description: "Timeout in seconds", default: 300 } }, required: ["urn"] } }; const GET_CHAT_MESSAGES_TOOL: Tool = { name: "get_linkedin_chat_messages", description: "Get top chat messages from LinkedIn management API. Account ID is taken from environment.", inputSchema: { type: "object", properties: { user: { type: "string", description: "User URN for filtering messages (must include prefix, e.g. fsd_profile:ACoAA...)" }, count: { type: "number", description: "Max messages to return", default: 20 }, timeout: { type: "number", description: "Timeout in seconds", default: 300 } }, required: ["user"] } }; const SEND_CHAT_MESSAGE_TOOL: Tool = { name: "send_linkedin_chat_message", description: "Send a chat message via LinkedIn management API. Account ID is taken from environment.", inputSchema: { type: "object", properties: { user: { type: "string", description: "Recipient user URN (must include prefix, e.g. fsd_profile:ACoAA...)" }, text: { type: "string", description: "Message text" }, timeout: { type: "number", description: "Timeout in seconds", default: 300 } }, required: ["user", "text"] } }; const SEND_CONNECTION_REQUEST_TOOL: Tool = { name: "send_linkedin_connection", description: "Send a connection invitation to LinkedIn user. Account ID is taken from environment.", inputSchema: { type: "object", properties: { user: { type: "string", description: "Recipient user URN (must include prefix, e.g. fsd_profile:ACoAA...)" }, timeout: { type: "number", description: "Timeout in seconds", default: 300 } }, required: ["user"] } }; const POST_COMMENT_TOOL: Tool = { name: "send_linkedin_post_comment", description: "Create a comment on a LinkedIn post or on another comment. Account ID is taken from environment.", inputSchema: { type: "object", properties: { text: { type: "string", description: "Comment text" }, urn: { type: "string", description: "URN of the activity or comment to comment on (e.g., 'activity:123' or 'comment:(activity:123,456)')" }, timeout: { type: "number", description: "Timeout in seconds", default: 300 } }, required: ["text", "urn"] } }; const SEND_LINKEDIN_POST_TOOL: Tool = { name: "send_linkedin_post", description: "Create a post on LinkedIn. Account ID is taken from environment.", inputSchema: { type: "object", properties: { text: { type: "string", description: "Post text content" }, visibility: { type: "string", description: "Post visibility", enum: ["ANYONE", "CONNECTIONS_ONLY"], default: "ANYONE" }, comment_scope: { type: "string", description: "Who can comment on the post", enum: ["ALL", "CONNECTIONS_ONLY", "NONE"], default: "ALL" }, timeout: { type: "number", description: "Timeout in seconds", default: 300 } }, required: ["text"] } }; const GET_USER_CONNECTIONS_TOOL: Tool = { name: "get_linkedin_user_connections", description: "Get list of LinkedIn user connections. Account ID is taken from environment.", inputSchema: { type: "object", properties: { connected_after: { type: "number", description: "Filter users that added after the specified date (timestamp)" }, count: { type: "number", description: "Max connections to return", default: 20 }, timeout: { type: "number", description: "Timeout in seconds", default: 300 } }, required: [] } }; const GET_LINKEDIN_POST_REPOSTS_TOOL: Tool = { name: "get_linkedin_post_reposts", description: "Get LinkedIn reposts for a post by URN", inputSchema: { type: "object", properties: { urn: { type: "string", description: "Post URN, only activity urn type is allowed (example: activity:7234173400267538433)" }, count: { type: "number", description: "Max reposts to return", default: 50 }, timeout: { type: "number", description: "Timeout in seconds", default: 300 } }, required: ["urn", "count"] } }; const GET_LINKEDIN_POST_COMMENTS_TOOL: Tool = { name: "get_linkedin_post_comments", description: "Get LinkedIn comments for a post by URN", inputSchema: { type: "object", properties: { urn: { type: "string", description: "Post URN, only activity urn type is allowed (example: activity:7234173400267538433)" }, sort: { type: "string", description: "Sort type (relevance or recent)", enum: ["relevance", "recent"], default: "relevance" }, count: { type: "number", description: "Max comments to return", default: 10 }, timeout: { type: "number", description: "Timeout in seconds", default: 300 } }, required: ["urn", "count"] } }; const GET_LINKEDIN_GOOGLE_COMPANY_TOOL: Tool = { name: "get_linkedin_google_company", description: "Search for LinkedIn companies using Google search. First result is usually the best match.", inputSchema: { type: "object", properties: { keywords: { type: "array", items: { type: "string" }, description: "Company keywords for search. For example, company name or company website", examples: [["Software as a Service (SaaS)"], ["google.com"]] }, with_urn: { type: "boolean", description: "Include URNs in response (increases execution time)", default: false }, count_per_keyword: { type: "number", description: "Max results per keyword", default: 1, minimum: 1, maximum: 10 }, timeout: { type: "number", description: "Timeout in seconds", default: 300 } }, required: ["keywords"] } }; const GET_LINKEDIN_COMPANY_TOOL: Tool = { name: "get_linkedin_company", description: "Get detailed information about a LinkedIn company", inputSchema: { type: "object", properties: { company: { type: "string", description: "Company Alias or URL or URN (example: 'openai' or 'company:1441')" }, timeout: { type: "number", description: "Timeout in seconds", default: 300 } }, required: ["company"] } }; const GET_LINKEDIN_COMPANY_EMPLOYEES_TOOL: Tool = { name: "get_linkedin_company_employees", description: "Get employees of a LinkedIn company", inputSchema: { type: "object", properties: { companies: { type: "array", items: { type: "string" }, description: "Company URNs (example: ['company:14064608'])" }, keywords: { type: "string", description: "Any keyword for searching employees", examples: ["Alex"] }, first_name: { type: "string", description: "Search for exact first name", examples: ["Bill"] }, last_name: { type: "string", description: "Search for exact last name", examples: ["Gates"] }, count: { type: "number", description: "Maximum number of results", default: 10 }, timeout: { type: "number", description: "Timeout in seconds", default: 300 } }, required: ["companies", "count"] } }; const server = new Server( { name: "hdw-mcp", version: "0.1.0" }, { capabilities: { resources: { supportedTypes: ["application/json", "text/plain"] }, tools: { linkedin: { description: "LinkedIn data access functionality" } } } } ); server.onerror = (error) => { log("MCP Server Error:", error); }; server.onclose = () => { log("MCP Server Connection Closed"); }; server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [ { uri: `linkedin://users/${encodeURIComponent(API_CONFIG.DEFAULT_QUERY)}`, name: `LinkedIn users for "${API_CONFIG.DEFAULT_QUERY}"`, mimeType: "application/json", description: "LinkedIn user search results including name, headline, and location" } ] })); server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const defaultQuery = API_CONFIG.DEFAULT_QUERY; if (request.params.uri !== `linkedin://users/${encodeURIComponent(defaultQuery)}`) { throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${request.params.uri}`); } try { const response = await makeRequest(API_CONFIG.ENDPOINTS.SEARCH_USERS, { keywords: defaultQuery, count: 10, timeout: 300 }); return { contents: [ { uri: request.params.uri, mimeType: "application/json", text: JSON.stringify(response, null, 2) } ] }; } catch (error) { log("Search error:", error); return { contents: [ { uri: request.params.uri, mimeType: "text/plain", text: `LinkedIn Search API error: ${formatError(error)}` } ], isError: true }; } }); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ SEARCH_LINKEDIN_USERS_TOOL, GET_LINKEDIN_PROFILE_TOOL, GET_LINKEDIN_EMAIL_TOOL, GET_LINKEDIN_USER_POSTS_TOOL, GET_LINKEDIN_USER_REACTIONS_TOOL, GET_CHAT_MESSAGES_TOOL, SEND_CHAT_MESSAGE_TOOL, SEND_CONNECTION_REQUEST_TOOL, POST_COMMENT_TOOL, GET_USER_CONNECTIONS_TOOL, GET_LINKEDIN_POST_REPOSTS_TOOL, GET_LINKEDIN_POST_COMMENTS_TOOL, GET_LINKEDIN_GOOGLE_COMPANY_TOOL, GET_LINKEDIN_COMPANY_TOOL, GET_LINKEDIN_COMPANY_EMPLOYEES_TOOL, SEND_LINKEDIN_POST_TOOL ] })); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; if (!args) throw new Error("No arguments provided"); switch (name) { case "search_linkedin_users": { if (!isValidLinkedinSearchUsersArgs(args)) { throw new McpError(ErrorCode.InvalidParams, "Invalid LinkedIn search arguments"); } const { keywords, first_name, last_name, title, company_keywords, school_keywords, current_company, past_company, location, industry, education, count = 10, timeout = 300 } = args as LinkedinSearchUsersArgs; const requestData: any = { timeout, count }; if (keywords) requestData.keywords = keywords; if (first_name) requestData.first_name = first_name; if (last_name) requestData.last_name = last_name; if (title) requestData.title = title; if (company_keywords) requestData.company_keywords = company_keywords; if (school_keywords) requestData.school_keywords = school_keywords; if (current_company) { requestData.current_company = typeof current_company === "string" && current_company.includes("company:") ? [{ type: "company", value: current_company.replace("company:", "") }] : current_company; } if (past_company) { requestData.past_company = typeof past_company === "string" && past_company.includes("company:") ? [{ type: "company", value: past_company.replace("company:", "") }] : past_company; } if (location) { requestData.location = typeof location === "string" && location.includes("geo:") ? [{ type: "geo", value: location.replace("geo:", "") }] : location; } if (industry) { requestData.industry = typeof industry === "string" && industry.includes("industry:") ? [{ type: "industry", value: industry.replace("industry:", "") }] : industry; } if (education) { requestData.education = typeof education === "string" && education.includes("fsd_company:") ? [{ type: "fsd_company", value: education.replace("fsd_company:", "") }] : education; } log("Starting LinkedIn users search with:", JSON.stringify(requestData)); try { const response = await makeRequest(API_CONFIG.ENDPOINTS.SEARCH_USERS, requestData); log(`Search complete, found ${response.length} results`); return { content: [ { type: "text", mimeType: "application/json", text: JSON.stringify(response, null, 2) } ] }; } catch (error) { log("LinkedIn search error:", error); return { content: [ { type: "text", mimeType: "text/plain", text: `LinkedIn search API error: ${formatError(error)}` } ], isError: true }; } } case "get_linkedin_profile": { if (!isValidLinkedinUserProfileArgs(args)) { throw new McpError(ErrorCode.InvalidParams, "Invalid LinkedIn profile arguments"); } const { user, with_experience = true, with_education = true, with_skills = true } = args as LinkedinUserProfileArgs; const requestData = { timeout: 300, user, with_experience, with_education, with_skills }; log("Starting LinkedIn profile lookup for:", user); try { const response = await makeRequest(API_CONFIG.ENDPOINTS.USER_PROFILE, requestData); return { content: [ { type: "text", mimeType: "application/json", text: JSON.stringify(response, null, 2) } ] }; } catch (error) { log("LinkedIn profile lookup error:", error); return { content: [ { type: "text", mimeType: "text/plain", text: `LinkedIn API error: ${formatError(error)}` } ], isError: true }; } } case "get_linkedin_email_user": { if (!isValidLinkedinEmailUserArgs(args)) { throw new McpError(ErrorCode.InvalidParams, "Invalid email parameter"); } const { email, count = 5, timeout = 300 } = args as LinkedinEmailUserArgs; const requestData = { timeout, email, count }; log("Starting LinkedIn email lookup for:", email); try { const response = await makeRequest(API_CONFIG.ENDPOINTS.LINKEDIN_EMAIL, requestData); return { content: [ { type: "text", mimeType: "application/json", text: JSON.stringify(response, null, 2) } ] }; } catch (error) { log("LinkedIn email lookup error:", error); return { content: [ { type: "text", mimeType: "text/plain", text: `LinkedIn email API error: ${formatError(error)}` } ], isError: true }; } } case "get_linkedin_user_posts": { if (!isValidLinkedinUserPostsArgs(args)) { throw new McpError(ErrorCode.InvalidParams, "Invalid user posts arguments"); } const { urn, count = 10, timeout = 300 } = args as LinkedinUserPostsArgs; const normalizedURN = normalizeUserURN(urn); if (!isValidUserURN(normalizedURN)) { throw new McpError(ErrorCode.InvalidParams, "Invalid URN format. Must start with 'fsd_profile:'"); } log("Starting LinkedIn user posts lookup for urn:", normalizedURN); const requestData = { timeout, urn: normalizedURN, count }; try { const response = await makeRequest(API_CONFIG.ENDPOINTS.LINKEDIN_USER_POSTS, requestData); return { content: [ { type: "text", mimeType: "application/json", text: JSON.stringify(response, null, 2) } ] }; } catch (error) { log("LinkedIn user posts lookup error:", error); return { content: [ { type: "text", mimeType: "text/plain", text: `LinkedIn user posts API error: ${formatError(error)}` } ], isError: true }; } } case "get_linkedin_user_reactions": { if (!isValidLinkedinUserReactionsArgs(args)) { throw new McpError(ErrorCode.InvalidParams, "Invalid user reactions arguments"); } const { urn, count = 10, timeout = 300 } = args as LinkedinUserReactionsArgs; const normalizedURN = normalizeUserURN(urn); if (!isValidUserURN(normalizedURN)) { throw new McpError(ErrorCode.InvalidParams, "Invalid URN format. Must start with 'fsd_profile:'"); } log("Starting LinkedIn user reactions lookup for urn:", normalizedURN); const requestData = { timeout, urn: normalizedURN, count }; try { const response = await makeRequest(API_CONFIG.ENDPOINTS.LINKEDIN_USER_REACTIONS, requestData); return { content: [ { type: "text", mimeType: "application/json", text: JSON.stringify(response, null, 2) } ] }; } catch (error) { log("LinkedIn user reactions lookup error:", error); return { content: [ { type: "text", mimeType: "text/plain", text: `LinkedIn user reactions API error: ${formatError(error)}` } ], isError: true }; } } case "get_linkedin_chat_messages": { if (!isValidLinkedinChatMessagesArgs(args)) { throw new McpError(ErrorCode.InvalidParams, "Invalid chat messages arguments"); } const { user, count = 20, timeout = 300 } = args as LinkedinChatMessagesArgs; const normalizedUser = normalizeUserURN(user); if (!isValidUserURN(normalizedUser)) { throw new McpError(ErrorCode.InvalidParams, "Invalid URN format. Must start with 'fsd_profile:'"); } const requestData = { timeout, user: normalizedUser, count, account_id: ACCOUNT_ID }; log("Starting LinkedIn chat messages lookup for user:", normalizedUser); try { const response = await makeRequest(API_CONFIG.ENDPOINTS.CHAT_MESSAGES, requestData, "GET"); return { content: [ { type: "text", mimeType: "application/json", text: JSON.stringify(response, null, 2) } ] }; } catch (error) { log("LinkedIn chat messages lookup error:", error); return { content: [ { type: "text", mimeType: "text/plain", text: `LinkedIn chat messages API error: ${formatError(error)}` } ], isError: true }; } } case "send_linkedin_chat_message": { if (!isValidSendLinkedinChatMessageArgs(args)) { throw new McpError(ErrorCode.InvalidParams, "Invalid parameters for sending chat message"); } const { user, text, timeout = 300 } = args as SendLinkedinChatMessageArgs; const normalizedUser = normalizeUserURN(user); if (!isValidUserURN(normalizedUser)) { throw new McpError(ErrorCode.InvalidParams, "Invalid URN format. Must start with 'fsd_profile:'"); } const requestData = { timeout, user: normalizedUser, text, account_id: ACCOUNT_ID }; log("Starting LinkedIn send chat message for user:", normalizedUser); try { const response = await makeRequest(API_CONFIG.ENDPOINTS.CHAT_MESSAGE, requestData, "POST"); return { content: [ { type: "text", mimeType: "application/json", text: JSON.stringify(response, null, 2) } ] }; } catch (error) { log("LinkedIn send chat message error:", error); return { content: [ { type: "text", mimeType: "text/plain", text: `LinkedIn send chat message API error: ${formatError(error)}` } ], isError: true }; } } case "send_linkedin_connection": { if (!isValidSendLinkedinConnectionArgs(args)) { throw new McpError(ErrorCode.InvalidParams, "Invalid parameters for connection request"); } const { user, timeout = 300 } = args as SendLinkedinConnectionArgs; const normalizedUser = normalizeUserURN(user); if (!isValidUserURN(normalizedUser)) { throw new McpError(ErrorCode.InvalidParams, "Invalid URN format. Must start with 'fsd_profile:'"); } const requestData = { timeout, user: normalizedUser, account_id: ACCOUNT_ID }; log("Sending LinkedIn connection request to user:", normalizedUser); try { const response = await makeRequest(API_CONFIG.ENDPOINTS.USER_CONNECTION, requestData, "POST"); return { content: [ { type: "text", mimeType: "application/json", text: JSON.stringify(response, null, 2) } ] }; } catch (error) { log("LinkedIn connection request error:", error); return { content: [ { type: "text", mimeType: "text/plain", text: `LinkedIn connection request API error: ${formatError(error)}` } ], isError: true }; } } case "send_linkedin_post_comment": { if (!isValidSendLinkedinPostCommentArgs(args)) { throw new McpError(ErrorCode.InvalidParams, "Invalid parameters for commenting on a post"); } const { text, urn, timeout = 300 } = args as SendLinkedinPostCommentArgs; const isActivityOrComment = urn.includes("activity:") || urn.includes("comment:"); if (!isActivityOrComment) { throw new McpError(ErrorCode.InvalidParams, "URN must be for an activity or comment"); } let urnObj; if (urn.startsWith("activity:")) { urnObj = { type: "activity", value: urn.replace("activity:", "") }; } else if (urn.startsWith("comment:")) { urnObj = { type: "comment", value: urn.replace("comment:", "") }; } else { urnObj = urn; } const requestData = { timeout, text, urn: urnObj, account_id: ACCOUNT_ID }; log(`Creating LinkedIn comment on ${urn}`); try { const response = await makeRequest(API_CONFIG.ENDPOINTS.POST_COMMENT, requestData, "POST"); return { content: [ { type: "text", mimeType: "application/json", text: JSON.stringify(response, null, 2) } ] }; } catch (error) { log("LinkedIn comment creation error:", error); return { content: [ { type: "text", mimeType: "text/plain", text: `LinkedIn comment API error: ${formatError(error)}` } ], isError: true }; } } case "send_linkedin_post": { if (!isValidSendLinkedinPostArgs(args)) { throw new McpError(ErrorCode.InvalidParams, "Invalid parameters for creating a LinkedIn post"); } const { text, visibility = "ANYONE", comment_scope = "ALL", timeout = 300 } = args as SendLinkedinPostArgs; const requestData = { text, visibility, comment_scope, timeout, account_id: ACCOUNT_ID }; log("Creating LinkedIn post with text:", text.substring(0, 50) + (text.length > 50 ? "..." : "")); try { const response = await makeRequest(API_CONFIG.ENDPOINTS.LINKEDIN_POST, requestData, "POST"); return { content: [ { type: "text", mimeType: "application/json", text: JSON.stringify(response, null, 2) } ] }; } catch (error) { log("LinkedIn post creation error:", error); return { content: [ { type: "text", mimeType: "text/plain", text: `LinkedIn post creation API error: ${formatError(error)}` } ], isError: true }; } } case "get_linkedin_user_connections": { if (!isValidGetLinkedinUserConnectionsArgs(args)) { throw new McpError(ErrorCode.InvalidParams, "Invalid user connections arguments"); } const { connected_after, count = 20, timeout = 300 } = args as GetLinkedinUserConnectionsArgs; const requestData: { timeout: number; account_id: string; connected_after?: number; count?: number; } = { timeout: Number(timeout), account_id: ACCOUNT_ID! }; if (connected_after != null) { requestData.connected_after = Number(connected_after); } if (count != null) { requestData.count = Number(count); } log("Starting LinkedIn user connections lookup"); try { const response = await makeRequest(API_CONFIG.ENDPOINTS.USER_CONNECTIONS, requestData, "GET"); return { content: [ { type: "text", mimeType: "application/json", text: JSON.stringify(response, null, 2) } ] }; } catch (error) { log("LinkedIn user connections lookup error:", error); return { content: [ { type: "text", mimeType: "text/plain", text: `LinkedIn user connections API error: ${formatError(error)}` } ], isError: true }; } } case "get_linkedin_post_reposts": { if (!isValidGetLinkedinPostRepostsArgs(args)) { throw new McpError(ErrorCode.InvalidParams, "Invalid post reposts arguments"); } const { urn, count = 10, timeout = 300 } = args as GetLinkedinPostRepostsArgs; const requestData = { timeout: Number(timeout), urn, count: Number(count) }; log(`Starting LinkedIn post reposts lookup for: ${urn}`); try { const response = await makeRequest(API_CONFIG.ENDPOINTS.LINKEDIN_POST_REPOSTS, requestData); return { content: [ { type: "text", mimeType: "application/json", text: JSON.stringify(response, null, 2) } ] }; } catch (error) { log("LinkedIn post reposts lookup error:", error); return { content: [ { type: "text", mimeType: "text/plain", text: `LinkedIn post reposts API error: ${formatError(error)}` } ], isError: true }; } } case "get_linkedin_post_comments": { if (!isValidGetLinkedinPostCommentsArgs(args)) { throw new McpError(ErrorCode.InvalidParams, "Invalid post comments arguments"); } const { urn, sort = "relevance", count = 10, timeout = 300 } = args as GetLinkedinPostCommentsArgs; const requestData = { timeout: Number(timeout), urn, sort, count: Number(count) }; log(`Starting LinkedIn post comments lookup for: ${urn}`); try { const response = await makeRequest(API_CONFIG.ENDPOINTS.LINKEDIN_POST_COMMENTS, requestData); return { content: [ { type: "text", mimeType: "application/json", text: JSON.stringify(response, null, 2) } ] }; } catch (error) { log("LinkedIn post comments lookup error:", error); return { content: [ { type: "text", mimeType: "text/plain", text: `LinkedIn post comments API error: ${formatError(error)}` } ], isError: true }; } } case "get_linkedin_google_company": { if (!isValidGetLinkedinGoogleCompanyArgs(args)) { throw new McpError(ErrorCode.InvalidParams, "Invalid Google company search arguments"); } const { keywords, with_urn = false, count_per_keyword = 1, timeout = 300 } = args as GetLinkedinGoogleCompanyArgs; const requestData = { timeout: Number(timeout), keywords, with_urn: Boolean(with_urn), count_per_keyword: Number(count_per_keyword) }; log(`Starting LinkedIn Google company search for keywords: ${keywords.join(', ')}`); try { const response = await makeRequest(API_CONFIG.ENDPOINTS.LINKEDIN_GOOGLE_COMPANY, requestData); return { content: [ { type: "text", mimeType: "application/json", text: JSON.stringify(response, null, 2) } ] }; } catch (error) { log("LinkedIn Google company search error:", error); return { content: [ { type: "text", mimeType: "text/plain", text: `LinkedIn Google company search API error: ${formatError(error)}` } ], isError: true }; } } case "get_linkedin_company": { if (!isValidGetLinkedinCompanyArgs(args)) { throw new McpError(ErrorCode.InvalidParams, "Invalid company arguments"); } const { company, timeout = 300 } = args as GetLinkedinCompanyArgs; const requestData = { timeout: Number(timeout), company }; log(`Starting LinkedIn company lookup for: ${company}`); try { const response = await makeRequest(API_CONFIG.ENDPOINTS.LINKEDIN_COMPANY, requestData); return { content: [ { type: "text", mimeType: "application/json", text: JSON.stringify(response, null, 2) } ] }; } catch (error) { log("LinkedIn company lookup error:", error); return { content: [ { type: "text", mimeType: "text/plain", text: `LinkedIn company API error: ${formatError(error)}` } ], isError: true }; } } case "get_linkedin_company_employees": { if (!isValidGetLinkedinCompanyEmployeesArgs(args)) { throw new McpError(ErrorCode.InvalidParams, "Invalid company employees arguments"); } const { companies, keywords, first_name, last_name, count = 10, timeout = 300 } = args as GetLinkedinCompanyEmployeesArgs; const requestData: { timeout: number; companies: string[]; keywords?: string; first_name?: string; last_name?: string; count: number; } = { timeout: Number(timeout), companies, count: Number(count) }; if (keywords != null && typeof keywords === 'string') { requestData.keywords = keywords; } if (first_name != null && typeof first_name === 'string') { requestData.first_name = first_name; } if (last_name != null && typeof last_name === 'string') { requestData.last_name = last_name; } log(`Starting LinkedIn company employees lookup for companies: ${companies.join(', ')}`); try { const response = await makeRequest(API_CONFIG.ENDPOINTS.LINKEDIN_COMPANY_EMPLOYEES, requestData); return { content: [ { type: "text", mimeType: "application/json", text: JSON.stringify(response, null, 2) } ] }; } catch (error) { log("LinkedIn company employees lookup error:", error); return { content: [ { type: "text", mimeType: "text/plain", text: `LinkedIn company employees API error: ${formatError(error)}` } ], isError: true }; } } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { log("Tool error:", error); return { content: [ { type: "text", mimeType: "text/plain", text: `API error: ${formatError(error)}` } ], isError: true }; } }); async function runServer() { const transport = new StdioServerTransport(); log("Starting HDW MCP Server..."); process.on("uncaughtException", (error) => { log("Uncaught Exception:", error); }); process.on("unhandledRejection", (reason, promise) => { log("Unhandled Rejection at:", promise, "reason:", reason); }); await server.connect(transport); log("HDW MCP Server running on stdio"); } runServer().catch((error) => { log("Fatal error running server:", error); process.exit(1); });