HDW MCP Server
- 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);
});