├── .env
├── .gitignore
├── .idea
├── .gitignore
├── inspectionProfiles
│ ├── Project_Default.xml
│ └── profiles_settings.xml
├── mcp.iml
├── modules.xml
└── vcs.xml
├── .npmrc
├── Dockerfile
├── LICENSE
├── LICENSE.md
├── README.md
├── glama.json
├── package-lock.json
├── package.json
├── smithery.yaml
├── src
├── index.ts
└── types.ts
└── tsconfig.json
/.env:
--------------------------------------------------------------------------------
1 | # Your Horizon Data Wave API access token
2 | HDW_ACCESS_TOKEN=
3 | HDW_ACCOUNT_ID=
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /build/
3 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
27 |
28 |
29 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/mcp.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npmjs.org/
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
2 | # Stage 1: build
3 | FROM node:lts-alpine AS build
4 | WORKDIR /app
5 |
6 | # Install dependencies without running prepare scripts (to avoid double build)
7 | COPY package.json package-lock.json* ./
8 | RUN npm install --ignore-scripts
9 |
10 | # Copy source and build
11 | COPY . .
12 | RUN npm run build
13 |
14 | # Stage 2: production image
15 | FROM node:lts-alpine
16 | WORKDIR /app
17 |
18 | # Copy built files and production dependencies
19 | COPY --from=build /app/build ./build
20 | COPY package.json package-lock.json* ./
21 | RUN npm install --omit=dev --ignore-scripts
22 |
23 | # Default environment
24 | ENV NODE_ENV=production
25 |
26 | # Entrypoint to start the MCP server
27 | ENTRYPOINT ["node","build/index.js"]
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Horizon Data Wave
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 HorizonDataWave
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # HDW MCP Server
2 | [](https://smithery.ai/server/@horizondatawave/hdw-mcp-server)
3 |
4 | A Model Context Protocol (MCP) server that provides comprehensive access to LinkedIn data and functionalities using the HorizonDataWave API, enabling not only data retrieval but also robust management of user accounts.
5 | ---
6 |
7 | ## Features
8 |
9 | - **LinkedIn Users Search:** Filter and search for LinkedIn users by keywords, name, title, company, location, industry, and education.
10 | - **Profile Lookup:** Retrieve detailed profile information for a LinkedIn user.
11 | - **Email Lookup:** Find LinkedIn user details by email address.
12 | - **Posts & Reactions:** Retrieve a user's posts and associated reactions.
13 | - **Post Reposts & Comments:** Retrieve reposts and comments for a specific LinkedIn post.
14 | - **Account Management:**
15 | - **Chat Functionality:** Retrieve and send chat messages via the LinkedIn management API.
16 | - **Connection Management:** Send connection invitations to LinkedIn users.
17 | - **Post Commenting:** Create comments on LinkedIn posts or replies.
18 | - **User Connections:** Retrieve a list of a user's LinkedIn connections.
19 | - **Company Search & Details:**
20 | - **Google Company Search:** Find LinkedIn companies using Google search – the first result is typically the best match.
21 | - **Company Lookup:** Retrieve detailed information about a LinkedIn company.
22 | - **Company Employees:** Retrieve employees for a given LinkedIn company.
23 |
24 | - **Google Search**
25 |
26 | ---
27 |
28 | ## Tools
29 |
30 | HDW MCP Server exposes several tools through the MCP protocol. Each tool is defined with its name, description, and input parameters:
31 |
32 | 1. **Search LinkedIn Users**
33 | **Name:** `search_linkedin_users`
34 | **Description:** Search for LinkedIn users with various filters.
35 | **Parameters:**
36 | - `keywords` (optional): Any keyword for search.
37 | - `first_name`, `last_name`, `title`, `company_keywords`, `school_keywords` (optional).
38 | - `current_company`, `past_company`, `location`, `industry`, `education` (optional).
39 | - `count` (optional, default: 10): Maximum number of results (max 1000).
40 | - `timeout` (optional, default: 300): Timeout in seconds (20–1500).
41 |
42 | 2. **Get LinkedIn Profile**
43 | **Name:** `get_linkedin_profile`
44 | **Description:** Retrieve detailed profile information about a LinkedIn user.
45 | **Parameters:**
46 | - `user` (required): User alias, URL, or URN.
47 | - `with_experience`, `with_education`, `with_skills` (optional, default: true).
48 |
49 | 3. **Get LinkedIn Email User**
50 | **Name:** `get_linkedin_email_user`
51 | **Description:** Look up LinkedIn user details by email.
52 | **Parameters:**
53 | - `email` (required): Email address.
54 | - `count` (optional, default: 5).
55 | - `timeout` (optional, default: 300).
56 |
57 | 4. **Get LinkedIn User Posts**
58 | **Name:** `get_linkedin_user_posts`
59 | **Description:** Retrieve posts for a LinkedIn user by URN.
60 | **Parameters:**
61 | - `urn` (required): User URN (must include prefix, e.g. `fsd_profile:...`).
62 | - `count` (optional, default: 10).
63 | - `timeout` (optional, default: 300).
64 |
65 | 5. **Get LinkedIn User Reactions**
66 | **Name:** `get_linkedin_user_reactions`
67 | **Description:** Retrieve reactions for a LinkedIn user by URN.
68 | **Parameters:**
69 | - `urn` (required).
70 | - `count` (optional, default: 10).
71 | - `timeout` (optional, default: 300).
72 |
73 | 6. **Get LinkedIn Chat Messages**
74 | **Name:** `get_linkedin_chat_messages`
75 | **Description:** Retrieve top chat messages from the LinkedIn management API.
76 | **Parameters:**
77 | - `user` (required): User URN (with prefix).
78 | - `count` (optional, default: 20).
79 | - `timeout` (optional, default: 300).
80 |
81 | 7. **Send LinkedIn Chat Message**
82 | **Name:** `send_linkedin_chat_message`
83 | **Description:** Send a chat message using the LinkedIn management API.
84 | **Parameters:**
85 | - `user` (required): Recipient user URN (with prefix).
86 | - `text` (required): Message text.
87 | - `timeout` (optional, default: 300).
88 |
89 | 8. **Send LinkedIn Connection Request**
90 | **Name:** `send_linkedin_connection`
91 | **Description:** Send a connection invitation to a LinkedIn user.
92 | **Parameters:**
93 | - `user` (required).
94 | - `timeout` (optional, default: 300).
95 |
96 | 9. **Send LinkedIn Post Comment**
97 | **Name:** `send_linkedin_post_comment`
98 | **Description:** Create a comment on a LinkedIn post or reply.
99 | **Parameters:**
100 | - `text` (required): Comment text.
101 | - `urn` (required): Activity or comment URN.
102 | - `timeout` (optional, default: 300).
103 |
104 | 10. **Get LinkedIn User Connections**
105 | **Name:** `get_linkedin_user_connections`
106 | **Description:** Retrieve a list of LinkedIn user connections.
107 | **Parameters:**
108 | - `connected_after` (optional): Timestamp filter.
109 | - `count` (optional, default: 20).
110 | - `timeout` (optional, default: 300).
111 |
112 | 11. **Get LinkedIn Post Reposts**
113 | **Name:** `get_linkedin_post_reposts`
114 | **Description:** Retrieve reposts for a LinkedIn post.
115 | **Parameters:**
116 | - `urn` (required): Post URN (must start with `activity:`).
117 | - `count` (optional, default: 10).
118 | - `timeout` (optional, default: 300).
119 |
120 | 12. **Get LinkedIn Post Comments**
121 | **Name:** `get_linkedin_post_comments`
122 | **Description:** Retrieve comments for a LinkedIn post.
123 | **Parameters:**
124 | - `urn` (required).
125 | - `sort` (optional, default: `"relevance"`; allowed values: `"relevance"`, `"recent"`).
126 | - `count` (optional, default: 10).
127 | - `timeout` (optional, default: 300).
128 |
129 | 13. **Get LinkedIn Google Company**
130 | **Name:** `get_linkedin_google_company`
131 | **Description:** Search for LinkedIn companies via Google – the first result is typically the best match.
132 | **Parameters:**
133 | - `keywords` (required): Array of company keywords.
134 | - `with_urn` (optional, default: false).
135 | - `count_per_keyword` (optional, default: 1; range 1–10).
136 | - `timeout` (optional, default: 300).
137 |
138 | 14. **Get LinkedIn Company**
139 | **Name:** `get_linkedin_company`
140 | **Description:** Retrieve detailed information about a LinkedIn company.
141 | **Parameters:**
142 | - `company` (required): Company alias, URL, or URN.
143 | - `timeout` (optional, default: 300).
144 |
145 | 15. **Get LinkedIn Company Employees**
146 | **Name:** `get_linkedin_company_employees`
147 | **Description:** Retrieve employees of a LinkedIn company.
148 | **Parameters:**
149 | - `companies` (required): Array of company URNs.
150 | - `keywords`, `first_name`, `last_name` (optional).
151 | - `count` (optional, default: 10).
152 | - `timeout` (optional, default: 300).
153 |
154 | ---
155 |
156 | ## Setup Guide
157 |
158 | ### Installing via Smithery
159 |
160 | To install HDW MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@horizondatawave/hdw-mcp-server):
161 |
162 | ```bash
163 | npx -y @smithery/cli install @horizondatawave/hdw-mcp-server --client claude
164 | ```
165 |
166 | ### 1. Clone the Repository (macOS)
167 |
168 | Open your terminal and run the following commands:
169 |
170 | ```bash
171 | # Clone the repository
172 | git clone https://github.com/horizondatawave/hdw-mcp-server.git
173 |
174 | # Change directory to the project folder
175 | cd hdw-mcp-server
176 |
177 | # Install dependencies
178 | npm install
179 | ```
180 | ### 2. Obtain Your API Credentials
181 |
182 | Register at [app.horizondatawave.ai](https://app.horizondatawave.ai) to get your API key and 100 free credits. You will receive your **HDW_ACCESS_TOKEN** and **HDW_ACCOUNT_ID**.
183 |
184 | ---
185 |
186 | ### 3. Configure the Environment
187 |
188 | Create a `.env` file in the root of your project with the following content:
189 |
190 | ```env
191 | HDW_ACCESS_TOKEN=YOUR_HD_W_ACCESS_TOKEN
192 | HDW_ACCOUNT_ID=YOUR_HD_W_ACCOUNT_ID
193 | ```
194 | ### 4. Client Configuration
195 |
196 | #### 4.1 Claude Desktop
197 |
198 | Update your Claude configuration file (`claude_desktop_config.json`) with the following content:
199 |
200 | ```json
201 | {
202 | "mcpServers": {
203 | "hdw": {
204 | "command": "npx",
205 | "args": ["-y","@horizondatawave/mcp"],
206 | "env": {
207 | "HDW_ACCESS_TOKEN": "YOUR_HD_W_ACCESS_TOKEN",
208 | "HDW_ACCOUNT_ID": "YOUR_HD_W_ACCOUNT_ID"
209 | }
210 | }
211 | }
212 | }
213 | ```
214 | *Configuration file location:*
215 |
216 | - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
217 | - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
218 |
219 | ---
220 |
221 | #### 4.2 Cursor
222 |
223 | **Easy way:**
224 | Open Cursor Settings and add a new MCP server with the command:
225 |
226 | ```bash
227 | env HDW_ACCESS_TOKEN=your-access-token HDW_ACCOUNT_ID=your-account-id node /path/to/your/build/index.js
228 | ```
229 | **Safe way:**
230 | Copy the provided template `run.template.sh` to a new file (e.g. `run.sh`), update it with your credentials, and configure Cursor to run:
231 |
232 | ```bash
233 | sh /path/to/your/run.sh
234 | ```
235 | #### 4.3 Windsurf
236 |
237 | Update your Windsurf configuration file (`mcp_config.json`) with the following content:
238 |
239 | ```json
240 | {
241 | "mcpServers": {
242 | "hdw": {
243 | "command": "node",
244 | "args": ["/path/to/your/build/index.js"],
245 | "env": {
246 | "HDW_ACCESS_TOKEN": "YOUR_HD_W_ACCESS_TOKEN",
247 | "HDW_ACCOUNT_ID": "YOUR_HD_W_ACCOUNT_ID"
248 | }
249 | }
250 | }
251 | }
252 | ```
253 | **Note:** After configuration, you can disable official web tools to conserve your API credits.
254 |
255 | ---
256 |
257 | ### MCP Client Example Configuration
258 |
259 | Below is an example configuration for an MCP client (e.g., a custom integration):
260 |
261 | ```json
262 | {
263 | "mcpServers": {
264 | "hdw": {
265 | "command": "npx",
266 | "args": ["-y","@horizondatawave/mcp"],
267 | "env": {
268 | "HDW_ACCESS_TOKEN": "YOUR_HD_W_ACCESS_TOKEN",
269 | "HDW_ACCOUNT_ID": "YOUR_HD_W_ACCOUNT_ID"
270 | }
271 | }
272 | }
273 | }
274 | ```
275 | Replace the paths and credentials with your own values.
276 | ## License
277 |
278 | This project is licensed under the [MIT License](LICENSE.md).
279 |
--------------------------------------------------------------------------------
/glama.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://glama.ai/mcp/schemas/server.json",
3 | "maintainers": [
4 | "4kulia"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@horizondatawave/mcp",
3 | "version": "0.1.3",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "@horizondatawave/mcp",
9 | "version": "0.1.3",
10 | "license": "MIT",
11 | "dependencies": {
12 | "@modelcontextprotocol/sdk": "^0.6.0",
13 | "dotenv": "^16.4.7",
14 | "zod": "^3.24.2"
15 | },
16 | "bin": {
17 | "hdw-mcp": "build/index.js",
18 | "horizondatawave-mcp": "build/index.js",
19 | "mcp": "build/index.js"
20 | },
21 | "devDependencies": {
22 | "@types/node": "^20.17.23",
23 | "typescript": "^5.8.2"
24 | },
25 | "engines": {
26 | "node": ">=18.0.0"
27 | }
28 | },
29 | "node_modules/@modelcontextprotocol/sdk": {
30 | "version": "0.6.1",
31 | "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.6.1.tgz",
32 | "integrity": "sha512-OkVXMix3EIbB5Z6yife2XTrSlOnVvCLR1Kg91I4pYFEsV9RbnoyQVScXCuVhGaZHOnTZgso8lMQN1Po2TadGKQ==",
33 | "license": "MIT",
34 | "dependencies": {
35 | "content-type": "^1.0.5",
36 | "raw-body": "^3.0.0",
37 | "zod": "^3.23.8"
38 | }
39 | },
40 | "node_modules/@types/node": {
41 | "version": "20.17.25",
42 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.25.tgz",
43 | "integrity": "sha512-bT+r2haIlplJUYtlZrEanFHdPIZTeiMeh/fSOEbOOfWf9uTn+lg8g0KU6Q3iMgjd9FLuuMAgfCNSkjUbxL6E3Q==",
44 | "dev": true,
45 | "license": "MIT",
46 | "dependencies": {
47 | "undici-types": "~6.19.2"
48 | }
49 | },
50 | "node_modules/bytes": {
51 | "version": "3.1.2",
52 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
53 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
54 | "license": "MIT",
55 | "engines": {
56 | "node": ">= 0.8"
57 | }
58 | },
59 | "node_modules/content-type": {
60 | "version": "1.0.5",
61 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
62 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
63 | "license": "MIT",
64 | "engines": {
65 | "node": ">= 0.6"
66 | }
67 | },
68 | "node_modules/depd": {
69 | "version": "2.0.0",
70 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
71 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
72 | "license": "MIT",
73 | "engines": {
74 | "node": ">= 0.8"
75 | }
76 | },
77 | "node_modules/dotenv": {
78 | "version": "16.4.7",
79 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
80 | "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
81 | "license": "BSD-2-Clause",
82 | "engines": {
83 | "node": ">=12"
84 | },
85 | "funding": {
86 | "url": "https://dotenvx.com"
87 | }
88 | },
89 | "node_modules/http-errors": {
90 | "version": "2.0.0",
91 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
92 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
93 | "license": "MIT",
94 | "dependencies": {
95 | "depd": "2.0.0",
96 | "inherits": "2.0.4",
97 | "setprototypeof": "1.2.0",
98 | "statuses": "2.0.1",
99 | "toidentifier": "1.0.1"
100 | },
101 | "engines": {
102 | "node": ">= 0.8"
103 | }
104 | },
105 | "node_modules/iconv-lite": {
106 | "version": "0.6.3",
107 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
108 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
109 | "license": "MIT",
110 | "dependencies": {
111 | "safer-buffer": ">= 2.1.2 < 3.0.0"
112 | },
113 | "engines": {
114 | "node": ">=0.10.0"
115 | }
116 | },
117 | "node_modules/inherits": {
118 | "version": "2.0.4",
119 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
120 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
121 | "license": "ISC"
122 | },
123 | "node_modules/raw-body": {
124 | "version": "3.0.0",
125 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
126 | "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
127 | "license": "MIT",
128 | "dependencies": {
129 | "bytes": "3.1.2",
130 | "http-errors": "2.0.0",
131 | "iconv-lite": "0.6.3",
132 | "unpipe": "1.0.0"
133 | },
134 | "engines": {
135 | "node": ">= 0.8"
136 | }
137 | },
138 | "node_modules/safer-buffer": {
139 | "version": "2.1.2",
140 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
141 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
142 | "license": "MIT"
143 | },
144 | "node_modules/setprototypeof": {
145 | "version": "1.2.0",
146 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
147 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
148 | "license": "ISC"
149 | },
150 | "node_modules/statuses": {
151 | "version": "2.0.1",
152 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
153 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
154 | "license": "MIT",
155 | "engines": {
156 | "node": ">= 0.8"
157 | }
158 | },
159 | "node_modules/toidentifier": {
160 | "version": "1.0.1",
161 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
162 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
163 | "license": "MIT",
164 | "engines": {
165 | "node": ">=0.6"
166 | }
167 | },
168 | "node_modules/typescript": {
169 | "version": "5.8.2",
170 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
171 | "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
172 | "dev": true,
173 | "license": "Apache-2.0",
174 | "bin": {
175 | "tsc": "bin/tsc",
176 | "tsserver": "bin/tsserver"
177 | },
178 | "engines": {
179 | "node": ">=14.17"
180 | }
181 | },
182 | "node_modules/undici-types": {
183 | "version": "6.19.8",
184 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
185 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
186 | "dev": true,
187 | "license": "MIT"
188 | },
189 | "node_modules/unpipe": {
190 | "version": "1.0.0",
191 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
192 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
193 | "license": "MIT",
194 | "engines": {
195 | "node": ">= 0.8"
196 | }
197 | },
198 | "node_modules/zod": {
199 | "version": "3.24.2",
200 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
201 | "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
202 | "license": "MIT",
203 | "funding": {
204 | "url": "https://github.com/sponsors/colinhacks"
205 | }
206 | }
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@horizondatawave/mcp",
3 | "version": "0.1.5",
4 | "description": "A Model Context Protocol (MCP) server that provides access to Horizon Data Wave's LinkedIn API",
5 | "private": false,
6 | "type": "module",
7 | "bin": {
8 | "@horizondatawave/mcp": "build/index.js",
9 | "hdw-mcp": "build/index.js",
10 | "horizondatawave-mcp": "build/index.js"
11 | },
12 | "files": [
13 | "build"
14 | ],
15 | "scripts": {
16 | "build": "tsc && chmod +x build/index.js",
17 | "prepare": "npm run build",
18 | "watch": "tsc --watch",
19 | "inspector": "npx @modelcontextprotocol/inspector build/index.js"
20 | },
21 | "dependencies": {
22 | "@modelcontextprotocol/sdk": "^0.6.0",
23 | "dotenv": "^16.4.7",
24 | "zod": "^3.24.2"
25 | },
26 | "devDependencies": {
27 | "@types/node": "^20.17.23",
28 | "typescript": "^5.8.2"
29 | },
30 | "keywords": [
31 | "mcp",
32 | "claude",
33 | "linkedin",
34 | "hdw",
35 | "horizondatawave"
36 | ],
37 | "license": "MIT",
38 | "engines": {
39 | "node": ">=18.0.0"
40 | },
41 | "repository": {
42 | "type": "git",
43 | "url": "https://github.com/horizondatawave/hdw-mcp-server.git"
44 | },
45 | "homepage": "https://horizondatawave.ai",
46 | "author": "Horizon Data Wave"
47 | }
48 |
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
2 |
3 | startCommand:
4 | type: stdio
5 | configSchema:
6 | # JSON Schema defining the configuration options for the MCP.
7 | type: object
8 | required:
9 | - hdwAccessToken
10 | - hdwAccountId
11 | properties:
12 | hdwAccessToken:
13 | type: string
14 | description: Your Horizon Data Wave access token
15 | hdwAccountId:
16 | type: string
17 | description: Your Horizon Data Wave account ID
18 | commandFunction:
19 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
20 | |-
21 | (config) => ({ command: 'node', args: ['build/index.js'], env: { HDW_ACCESS_TOKEN: config.hdwAccessToken, HDW_ACCOUNT_ID: config.hdwAccountId } })
22 | exampleConfig:
23 | hdwAccessToken: YOUR_HD_W_ACCESS_TOKEN
24 | hdwAccountId: YOUR_ACCOUNT_ID
25 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4 | import {
5 | ListResourcesRequestSchema,
6 | ReadResourceRequestSchema,
7 | ListToolsRequestSchema,
8 | CallToolRequestSchema,
9 | ErrorCode,
10 | McpError,
11 | Tool
12 | } from "@modelcontextprotocol/sdk/types.js";
13 | import dotenv from "dotenv";
14 | import https from "https";
15 | import { Buffer } from "buffer";
16 | import {
17 | LinkedinSearchUsersArgs,
18 | LinkedinUserProfileArgs,
19 | LinkedinEmailUserArgs,
20 | LinkedinUserPostsArgs,
21 | LinkedinUserReactionsArgs,
22 | LinkedinChatMessagesArgs,
23 | SendLinkedinChatMessageArgs,
24 | SendLinkedinConnectionArgs,
25 | SendLinkedinPostCommentArgs,
26 | GetLinkedinUserConnectionsArgs,
27 | GetLinkedinPostRepostsArgs,
28 | GetLinkedinPostCommentsArgs,
29 | GetLinkedinGoogleCompanyArgs,
30 | GetLinkedinCompanyArgs,
31 | GetLinkedinCompanyEmployeesArgs,
32 | SendLinkedinPostArgs,
33 | LinkedinSalesNavigatorSearchUsersArgs,
34 | LinkedinManagementConversationsPayload,
35 | GoogleSearchPayload,
36 | isValidLinkedinSearchUsersArgs,
37 | isValidLinkedinUserProfileArgs,
38 | isValidLinkedinEmailUserArgs,
39 | isValidLinkedinUserPostsArgs,
40 | isValidLinkedinUserReactionsArgs,
41 | isValidLinkedinChatMessagesArgs,
42 | isValidSendLinkedinChatMessageArgs,
43 | isValidSendLinkedinConnectionArgs,
44 | isValidSendLinkedinPostCommentArgs,
45 | isValidGetLinkedinUserConnectionsArgs,
46 | isValidGetLinkedinPostRepostsArgs,
47 | isValidGetLinkedinPostCommentsArgs,
48 | isValidGetLinkedinGoogleCompanyArgs,
49 | isValidGetLinkedinCompanyArgs,
50 | isValidGetLinkedinCompanyEmployeesArgs,
51 | isValidSendLinkedinPostArgs,
52 | isValidLinkedinSalesNavigatorSearchUsersArgs,
53 | isValidLinkedinManagementConversationsArgs,
54 | isValidGoogleSearchPayload
55 | } from "./types.js";
56 |
57 | try {
58 | dotenv.config();
59 | if (process.env.HOME) {
60 | dotenv.config({ path: `${process.env.HOME}/.hdw-mcp.env` });
61 | }
62 | } catch (error) {
63 | console.error("Error loading .env file:", error);
64 | }
65 |
66 | const API_KEY = process.env.HDW_ACCESS_TOKEN;
67 | const ACCOUNT_ID = process.env.HDW_ACCOUNT_ID;
68 |
69 | if (!API_KEY) {
70 | console.error("Error: HDW_ACCESS_TOKEN environment variable is required");
71 | process.exit(1);
72 | }
73 | if (!ACCOUNT_ID) {
74 | console.error("Warning: HDW_ACCOUNT_ID environment variable is required for chat endpoints");
75 | }
76 |
77 | const API_CONFIG = {
78 | BASE_URL: "https://api.horizondatawave.ai",
79 | DEFAULT_QUERY: "software engineer",
80 | ENDPOINTS: {
81 | SEARCH_USERS: "/api/linkedin/search/users",
82 | USER_PROFILE: "/api/linkedin/user",
83 | USER_EXPERIENCE: "/api/linkedin/user/experience",
84 | USER_EDUCATION: "/api/linkedin/user/education",
85 | USER_SKILLS: "/api/linkedin/user/skills",
86 | LINKEDIN_EMAIL: "/api/linkedin/email/user",
87 | LINKEDIN_USER_POSTS: "/api/linkedin/user/posts",
88 | LINKEDIN_USER_REACTIONS: "/api/linkedin/user/reactions",
89 | CHAT_MESSAGES: "/api/linkedin/management/chat/messages",
90 | CHAT_MESSAGE: "/api/linkedin/management/chat/message",
91 | USER_CONNECTION: "/api/linkedin/management/user/connection",
92 | USER_CONNECTIONS: "/api/linkedin/management/user/connections",
93 | POST_COMMENT: "/api/linkedin/management/post/comment",
94 | LINKEDIN_POST: "/api/linkedin/management/post",
95 | LINKEDIN_POST_REPOSTS: "/api/linkedin/post/reposts",
96 | LINKEDIN_POST_COMMENTS: "/api/linkedin/post/comments",
97 | LINKEDIN_GOOGLE_COMPANY: "/api/linkedin/google/company",
98 | LINKEDIN_COMPANY: "/api/linkedin/company",
99 | LINKEDIN_COMPANY_EMPLOYEES: "/api/linkedin/company/employees",
100 | LINKEDIN_SN_SEARCH_USERS: "/api/linkedin/sn_search/users",
101 | CONVERSATIONS: "/api/linkedin/management/conversations",
102 | GOOGLE_SEARCH: "/api/google/search",
103 | }
104 | } as const;
105 |
106 | function formatError(error: unknown): string {
107 | if (error instanceof Error) return error.message;
108 | return String(error);
109 | }
110 |
111 | function log(message: string, ...args: any[]) {
112 | console.error(`[${new Date().toISOString()}] ${message}`, ...args);
113 | }
114 |
115 |
116 | async function makeRequest(endpoint: string, data: any, method: string = "POST"): Promise {
117 | const baseUrl = API_CONFIG.BASE_URL.replace(/\/+$/, "");
118 | const url = baseUrl + (endpoint.startsWith("/") ? endpoint : `/${endpoint}`);
119 | const headers = new Headers();
120 | headers.append("Content-Type", "application/json");
121 | headers.append("access-token", API_KEY!);
122 | const options: RequestInit = {
123 | method,
124 | headers,
125 | body: JSON.stringify(data)
126 | };
127 | log(`Making ${method} request to ${endpoint} with data: ${JSON.stringify(data)}`);
128 | const startTime = Date.now();
129 | try {
130 | const response = await fetch(url, options);
131 | if (!response.ok) {
132 | const errorData = await response.json().catch(() => ({}));
133 | throw new Error(`API error: ${response.status} ${errorData.message || response.statusText}`);
134 | }
135 | const result = await response.json();
136 | log(`API request to ${endpoint} completed in ${Date.now() - startTime}ms`);
137 | return result;
138 | } catch (error) {
139 | log(`API request to ${endpoint} failed after ${Date.now() - startTime}ms:`, error);
140 | throw error;
141 | }
142 | }
143 |
144 | function normalizeUserURN(urn: string): string {
145 | if (!urn.includes(":")) {
146 | log(`Warning: URN format might be missing a prefix. Adding "fsd_profile:" to: ${urn}`);
147 | return `fsd_profile:${urn}`;
148 | }
149 | return urn;
150 | }
151 |
152 | function isValidUserURN(urn: string): boolean {
153 | return urn.startsWith("fsd_profile:");
154 | }
155 |
156 | const SEARCH_LINKEDIN_USERS_TOOL: Tool = {
157 | name: "search_linkedin_users",
158 | description: "Search for LinkedIn users with various filters like keywords, name, title, company, location etc.",
159 | inputSchema: {
160 | type: "object",
161 | properties: {
162 | keywords: { type: "string", description: "Any keyword for searching in the user page." },
163 | first_name: { type: "string", description: "Exact first name" },
164 | last_name: { type: "string", description: "Exact last name" },
165 | title: { type: "string", description: "Exact word in the title" },
166 | company_keywords: { type: "string", description: "Exact word in the company name" },
167 | school_keywords: { type: "string", description: "Exact word in the school name" },
168 | current_company: { type: "string", description: "Company URN or name" },
169 | past_company: { type: "string", description: "Past company URN or name" },
170 | location: { type: "string", description: "Location name or URN" },
171 | industry: { type: "string", description: "Industry URN or name" },
172 | education: { type: "string", description: "Education URN or name" },
173 | count: { type: "number", description: "Maximum number of results (max 1000)", default: 10 },
174 | timeout: { type: "number", description: "Timeout in seconds (20-1500)", default: 300 }
175 | },
176 | required: ["count"]
177 | }
178 | };
179 |
180 | const GET_LINKEDIN_PROFILE_TOOL: Tool = {
181 | name: "get_linkedin_profile",
182 | description: "Get detailed information about a LinkedIn user profile",
183 | inputSchema: {
184 | type: "object",
185 | properties: {
186 | user: { type: "string", description: "User alias, URL, or URN" },
187 | with_experience: { type: "boolean", description: "Include experience info", default: true },
188 | with_education: { type: "boolean", description: "Include education info", default: true },
189 | with_skills: { type: "boolean", description: "Include skills info", default: true }
190 | },
191 | required: ["user"]
192 | }
193 | };
194 |
195 | const GET_LINKEDIN_EMAIL_TOOL: Tool = {
196 | name: "get_linkedin_email_user",
197 | description: "Get LinkedIn user details by email",
198 | inputSchema: {
199 | type: "object",
200 | properties: {
201 | email: { type: "string", description: "Email address" },
202 | count: { type: "number", description: "Max results", default: 5 },
203 | timeout: { type: "number", description: "Timeout in seconds", default: 300 }
204 | },
205 | required: ["email"]
206 | }
207 | };
208 |
209 | const GET_LINKEDIN_USER_POSTS_TOOL: Tool = {
210 | name: "get_linkedin_user_posts",
211 | description: "Get LinkedIn posts for a user by URN (must include prefix, example: fsd_profile:ACoAAEWn01QBWENVMWqyM3BHfa1A-xsvxjdaXsY)",
212 | inputSchema: {
213 | type: "object",
214 | properties: {
215 | urn: { type: "string", description: "User URN (must include prefix, example: fsd_profile:ACoAA...)" },
216 | count: { type: "number", description: "Max posts", default: 10 },
217 | timeout: { type: "number", description: "Timeout in seconds", default: 300 }
218 | },
219 | required: ["urn"]
220 | }
221 | };
222 |
223 | const GET_LINKEDIN_USER_REACTIONS_TOOL: Tool = {
224 | name: "get_linkedin_user_reactions",
225 | description: "Get LinkedIn reactions for a user by URN (must include prefix, example: fsd_profile:ACoAA...)",
226 | inputSchema: {
227 | type: "object",
228 | properties: {
229 | urn: { type: "string", description: "User URN (must include prefix, example: fsd_profile:ACoAA...)" },
230 | count: { type: "number", description: "Max reactions", default: 10 },
231 | timeout: { type: "number", description: "Timeout in seconds", default: 300 }
232 | },
233 | required: ["urn"]
234 | }
235 | };
236 |
237 | const GET_CHAT_MESSAGES_TOOL: Tool = {
238 | name: "get_linkedin_chat_messages",
239 | description: "Get top chat messages from LinkedIn management API. Account ID is taken from environment.",
240 | inputSchema: {
241 | type: "object",
242 | properties: {
243 | user: { type: "string", description: "User URN for filtering messages (must include prefix, e.g. fsd_profile:ACoAA...)" },
244 | company: { type: "string", description: "Company URN where the account is admin (format: company:123456)", default: undefined },
245 | count: { type: "number", description: "Max messages to return", default: 20 },
246 | timeout: { type: "number", description: "Timeout in seconds", default: 300 }
247 | },
248 | required: ["user"]
249 | }
250 | };
251 |
252 | const SEND_CHAT_MESSAGE_TOOL: Tool = {
253 | name: "send_linkedin_chat_message",
254 | description: "Send a chat message via LinkedIn management API. Account ID is taken from environment.",
255 | inputSchema: {
256 | type: "object",
257 | properties: {
258 | user: { type: "string", description: "Recipient user URN (must include prefix, e.g. fsd_profile:ACoAA...)" },
259 | company: { type: "string", description: "Company URN where the account is admin (format: company:123456)", default: undefined },
260 | text: { type: "string", description: "Message text" },
261 | timeout: { type: "number", description: "Timeout in seconds", default: 300 }
262 | },
263 | required: ["user", "text"]
264 | }
265 | };
266 |
267 | const SEND_CONNECTION_REQUEST_TOOL: Tool = {
268 | name: "send_linkedin_connection",
269 | description: "Send a connection invitation to LinkedIn user. Account ID is taken from environment.",
270 | inputSchema: {
271 | type: "object",
272 | properties: {
273 | user: { type: "string", description: "Recipient user URN (must include prefix, e.g. fsd_profile:ACoAA...)" },
274 | timeout: { type: "number", description: "Timeout in seconds", default: 300 }
275 | },
276 | required: ["user"]
277 | }
278 | };
279 |
280 | const POST_COMMENT_TOOL: Tool = {
281 | name: "send_linkedin_post_comment",
282 | description: "Create a comment on a LinkedIn post or on another comment. Account ID is taken from environment.",
283 | inputSchema: {
284 | type: "object",
285 | properties: {
286 | text: { type: "string", description: "Comment text" },
287 | urn: {
288 | type: "string",
289 | description: "URN of the activity or comment to comment on (e.g., 'activity:123' or 'comment:(activity:123,456)')"
290 | },
291 | timeout: { type: "number", description: "Timeout in seconds", default: 300 }
292 | },
293 | required: ["text", "urn"]
294 | }
295 | };
296 |
297 | const SEND_LINKEDIN_POST_TOOL: Tool = {
298 | name: "send_linkedin_post",
299 | description: "Create a post on LinkedIn. Account ID is taken from environment.",
300 | inputSchema: {
301 | type: "object",
302 | properties: {
303 | text: { type: "string", description: "Post text content" },
304 | visibility: {
305 | type: "string",
306 | description: "Post visibility",
307 | enum: ["ANYONE", "CONNECTIONS_ONLY"],
308 | default: "ANYONE"
309 | },
310 | comment_scope: {
311 | type: "string",
312 | description: "Who can comment on the post",
313 | enum: ["ALL", "CONNECTIONS_ONLY", "NONE"],
314 | default: "ALL"
315 | },
316 | timeout: { type: "number", description: "Timeout in seconds", default: 300 }
317 | },
318 | required: ["text"]
319 | }
320 | };
321 |
322 | const GET_USER_CONNECTIONS_TOOL: Tool = {
323 | name: "get_linkedin_user_connections",
324 | description: "Get list of LinkedIn user connections. Account ID is taken from environment.",
325 | inputSchema: {
326 | type: "object",
327 | properties: {
328 | connected_after: { type: "number", description: "Filter users that added after the specified date (timestamp)" },
329 | count: { type: "number", description: "Max connections to return", default: 20 },
330 | timeout: { type: "number", description: "Timeout in seconds", default: 300 }
331 | },
332 | required: []
333 | }
334 | };
335 |
336 | const GET_LINKEDIN_POST_REPOSTS_TOOL: Tool = {
337 | name: "get_linkedin_post_reposts",
338 | description: "Get LinkedIn reposts for a post by URN",
339 | inputSchema: {
340 | type: "object",
341 | properties: {
342 | urn: { type: "string", description: "Post URN, only activity urn type is allowed (example: activity:7234173400267538433)" },
343 | count: { type: "number", description: "Max reposts to return", default: 50 },
344 | timeout: { type: "number", description: "Timeout in seconds", default: 300 }
345 | },
346 | required: ["urn", "count"]
347 | }
348 | };
349 |
350 | const GET_LINKEDIN_POST_COMMENTS_TOOL: Tool = {
351 | name: "get_linkedin_post_comments",
352 | description: "Get LinkedIn comments for a post by URN",
353 | inputSchema: {
354 | type: "object",
355 | properties: {
356 | urn: { type: "string", description: "Post URN, only activity urn type is allowed (example: activity:7234173400267538433)" },
357 | sort: { type: "string", description: "Sort type (relevance or recent)", enum: ["relevance", "recent"], default: "relevance" },
358 | count: { type: "number", description: "Max comments to return", default: 10 },
359 | timeout: { type: "number", description: "Timeout in seconds", default: 300 }
360 | },
361 | required: ["urn", "count"]
362 | }
363 | };
364 |
365 | const GET_LINKEDIN_GOOGLE_COMPANY_TOOL: Tool = {
366 | name: "get_linkedin_google_company",
367 | description: "Search for LinkedIn companies using Google search. First result is usually the best match.",
368 | inputSchema: {
369 | type: "object",
370 | properties: {
371 | keywords: {
372 | type: "array",
373 | items: { type: "string" },
374 | description: "Company keywords for search. For example, company name or company website",
375 | examples: [["Software as a Service (SaaS)"], ["google.com"]]
376 | },
377 | with_urn: { type: "boolean", description: "Include URNs in response (increases execution time)", default: false },
378 | count_per_keyword: { type: "number", description: "Max results per keyword", default: 1, minimum: 1, maximum: 10 },
379 | timeout: { type: "number", description: "Timeout in seconds", default: 300 }
380 | },
381 | required: ["keywords"]
382 | }
383 | };
384 |
385 | const GET_LINKEDIN_COMPANY_TOOL: Tool = {
386 | name: "get_linkedin_company",
387 | description: "Get detailed information about a LinkedIn company",
388 | inputSchema: {
389 | type: "object",
390 | properties: {
391 | company: { type: "string", description: "Company Alias or URL or URN (example: 'openai' or 'company:1441')" },
392 | timeout: { type: "number", description: "Timeout in seconds", default: 300 }
393 | },
394 | required: ["company"]
395 | }
396 | };
397 |
398 | const GET_LINKEDIN_COMPANY_EMPLOYEES_TOOL: Tool = {
399 | name: "get_linkedin_company_employees",
400 | description: "Get employees of a LinkedIn company",
401 | inputSchema: {
402 | type: "object",
403 | properties: {
404 | companies: {
405 | type: "array",
406 | items: { type: "string" },
407 | description: "Company URNs (example: ['company:14064608'])"
408 | },
409 | keywords: { type: "string", description: "Any keyword for searching employees", examples: ["Alex"] },
410 | first_name: { type: "string", description: "Search for exact first name", examples: ["Bill"] },
411 | last_name: { type: "string", description: "Search for exact last name", examples: ["Gates"] },
412 | count: { type: "number", description: "Maximum number of results", default: 10 },
413 | timeout: { type: "number", description: "Timeout in seconds", default: 300 }
414 | },
415 | required: ["companies", "count"]
416 | }
417 | };
418 |
419 | const LINKEDIN_SN_SEARCH_USERS_TOOL: Tool = {
420 | name: "linkedin_sn_search_users",
421 | description: "Advanced search for LinkedIn users using Sales Navigator filters",
422 | inputSchema: {
423 | type: "object",
424 | properties: {
425 | keywords: {
426 | type: "string",
427 | description: "Any keyword for searching in the user profile. Using this may reduce result count."
428 | },
429 | first_names: {
430 | type: "array",
431 | items: { type: "string" },
432 | description: "Exact first names to search for"
433 | },
434 | last_names: {
435 | type: "array",
436 | items: { type: "string" },
437 | description: "Exact last names to search for"
438 | },
439 | current_titles: {
440 | type: "array",
441 | items: { type: "string" },
442 | description: "Exact words to search in current titles"
443 | },
444 | location: {
445 | type: ["string", "array"],
446 | items: { type: "string" },
447 | description: "Location URN (geo:*) or name, or array of them"
448 | },
449 | education: {
450 | type: ["string", "array"],
451 | items: { type: "string" },
452 | description: "Education URN (company:*) or name, or array of them"
453 | },
454 | languages: {
455 | type: "array",
456 | items: {
457 | type: "string",
458 | enum: [
459 | "Arabic", "English", "Spanish", "Portuguese", "Chinese",
460 | "French", "Italian", "Russian", "German", "Dutch",
461 | "Turkish", "Tagalog", "Polish", "Korean", "Japanese",
462 | "Malay", "Norwegian", "Danish", "Romanian", "Swedish",
463 | "Bahasa Indonesia", "Czech"
464 | ]
465 | },
466 | description: "Profile languages"
467 | },
468 | past_titles: {
469 | type: "array",
470 | items: { type: "string" },
471 | description: "Exact words to search in past titles"
472 | },
473 | functions: {
474 | type: "array",
475 | items: {
476 | type: "string",
477 | enum: [
478 | "Accounting", "Administrative", "Arts and Design", "Business", "Development",
479 | "Community and Social Services", "Consulting", "Education", "Engineering",
480 | "Entrepreneurship", "Finance", "Healthcare Services", "Human Resources",
481 | "Information Technology", "Legal", "Marketing", "Media and Communication",
482 | "Military and Protective Services", "Operations", "Product Management",
483 | "Program and Project Management", "Purchasing", "Quality Assurance",
484 | "Research", "Real Estate", "Sales", "Customer Success and Support"
485 | ]
486 | },
487 | description: "Job functions"
488 | },
489 | levels: {
490 | type: "array",
491 | items: {
492 | type: "string",
493 | enum: [
494 | "Entry", "Director", "Owner", "CXO", "Vice President",
495 | "Experienced Manager", "Entry Manager", "Strategic", "Senior", "Trainy"
496 | ]
497 | },
498 | description: "Job seniority levels"
499 | },
500 | years_in_the_current_company: {
501 | type: "array",
502 | items: {
503 | type: "string",
504 | enum: ["0-1", "1-2", "3-5", "6-10", "10+"]
505 | },
506 | description: "Years in current company ranges"
507 | },
508 | years_in_the_current_position: {
509 | type: "array",
510 | items: {
511 | type: "string",
512 | enum: ["0-1", "1-2", "3-5", "6-10", "10+"]
513 | },
514 | description: "Years in current position ranges"
515 | },
516 | company_sizes: {
517 | type: "array",
518 | items: {
519 | type: "string",
520 | enum: [
521 | "Self-employed", "1-10", "11-50", "51-200", "201-500",
522 | "501-1,000", "1,001-5,000", "5,001-10,000", "10,001+"
523 | ]
524 | },
525 | description: "Company size ranges"
526 | },
527 | company_types: {
528 | type: "array",
529 | items: {
530 | type: "string",
531 | enum: [
532 | "Public Company", "Privately Held", "Non Profit",
533 | "Educational Institution", "Partnership", "Self Employed",
534 | "Self Owned", "Government Agency"
535 | ]
536 | },
537 | description: "Company types"
538 | },
539 | company_locations: {
540 | type: ["string", "array"],
541 | items: { type: "string" },
542 | description: "Company location URN (geo:*) or name, or array of them"
543 | },
544 | current_companies: {
545 | type: ["string", "array"],
546 | items: { type: "string" },
547 | description: "Current company URN (company:*) or name, or array of them"
548 | },
549 | past_companies: {
550 | type: ["string", "array"],
551 | items: { type: "string" },
552 | description: "Past company URN (company:*) or name, or array of them"
553 | },
554 | industry: {
555 | type: ["string", "array"],
556 | items: { type: "string" },
557 | description: "Industry URN (industry:*) or name, or array of them"
558 | },
559 | count: {
560 | type: "number",
561 | description: "Maximum number of results (max 2500)",
562 | default: 10,
563 | minimum: 1,
564 | maximum: 2500
565 | },
566 | timeout: {
567 | type: "number",
568 | description: "Timeout in seconds (20-1500)",
569 | default: 300,
570 | minimum: 20,
571 | maximum: 1500
572 | }
573 | },
574 | required: ["count"]
575 | }
576 | };
577 |
578 | const GET_LINKEDIN_CONVERSATIONS_TOOL: Tool = {
579 | name: "get_linkedin_conversations",
580 | description: "Get list of LinkedIn conversations from the messaging interface. Account ID is taken from environment.",
581 | inputSchema: {
582 | type: "object",
583 | properties: {
584 | connected_after: { type: "number", description: "Filter conversations created after the specified date (timestamp)" },
585 | count: { type: "number", description: "Max conversations to return", default: 20 },
586 | timeout: { type: "number", description: "Timeout in seconds", default: 300 }
587 | },
588 | required: []
589 | }
590 | };
591 |
592 | const GOOGLE_SEARCH_TOOL: Tool = {
593 | name: "google_search",
594 | description: "Search for information using Google search API",
595 | inputSchema: {
596 | type: "object",
597 | properties: {
598 | query: { type: "string", description: "Search query. For example: 'python fastapi'" },
599 | count: { type: "number", description: "Maximum number of results (from 1 to 20)", default: 10 },
600 | timeout: { type: "number", description: "Timeout in seconds (20-1500)", default: 300 }
601 | },
602 | required: ["query"]
603 | }
604 | };
605 |
606 | const server = new Server(
607 | { name: "hdw-mcp", version: "0.1.0" },
608 | {
609 | capabilities: {
610 | resources: { supportedTypes: ["application/json", "text/plain"] },
611 | tools: { linkedin: { description: "LinkedIn data access functionality" } }
612 | }
613 | }
614 | );
615 |
616 | server.onerror = (error) => {
617 | log("MCP Server Error:", error);
618 | };
619 |
620 | server.onclose = () => {
621 | log("MCP Server Connection Closed");
622 | };
623 |
624 | server.setRequestHandler(ListResourcesRequestSchema, async () => ({
625 | resources: [
626 | {
627 | uri: `linkedin://users/${encodeURIComponent(API_CONFIG.DEFAULT_QUERY)}`,
628 | name: `LinkedIn users for "${API_CONFIG.DEFAULT_QUERY}"`,
629 | mimeType: "application/json",
630 | description: "LinkedIn user search results including name, headline, and location"
631 | }
632 | ]
633 | }));
634 |
635 |
636 | server.setRequestHandler(ListToolsRequestSchema, async () => ({
637 | tools: [
638 | SEARCH_LINKEDIN_USERS_TOOL,
639 | GET_LINKEDIN_PROFILE_TOOL,
640 | GET_LINKEDIN_EMAIL_TOOL,
641 | GET_LINKEDIN_USER_POSTS_TOOL,
642 | GET_LINKEDIN_USER_REACTIONS_TOOL,
643 | GET_CHAT_MESSAGES_TOOL,
644 | SEND_CHAT_MESSAGE_TOOL,
645 | SEND_CONNECTION_REQUEST_TOOL,
646 | POST_COMMENT_TOOL,
647 | GET_USER_CONNECTIONS_TOOL,
648 | GET_LINKEDIN_POST_REPOSTS_TOOL,
649 | GET_LINKEDIN_POST_COMMENTS_TOOL,
650 | GET_LINKEDIN_GOOGLE_COMPANY_TOOL,
651 | GET_LINKEDIN_COMPANY_TOOL,
652 | GET_LINKEDIN_COMPANY_EMPLOYEES_TOOL,
653 | SEND_LINKEDIN_POST_TOOL,
654 | LINKEDIN_SN_SEARCH_USERS_TOOL,
655 | GET_LINKEDIN_CONVERSATIONS_TOOL,
656 | GOOGLE_SEARCH_TOOL
657 | ]
658 | }));
659 |
660 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
661 | try {
662 | const { name, arguments: args } = request.params;
663 | if (!args) throw new Error("No arguments provided");
664 |
665 | switch (name) {
666 | case "search_linkedin_users": {
667 | if (!isValidLinkedinSearchUsersArgs(args)) {
668 | throw new McpError(ErrorCode.InvalidParams, "Invalid LinkedIn search arguments");
669 | }
670 | const {
671 | keywords, first_name, last_name, title, company_keywords,
672 | school_keywords, current_company, past_company, location,
673 | industry, education, count = 10, timeout = 300
674 | } = args as LinkedinSearchUsersArgs;
675 | const requestData: any = { timeout, count };
676 | if (keywords) requestData.keywords = keywords;
677 | if (first_name) requestData.first_name = first_name;
678 | if (last_name) requestData.last_name = last_name;
679 | if (title) requestData.title = title;
680 | if (company_keywords) requestData.company_keywords = company_keywords;
681 | if (school_keywords) requestData.school_keywords = school_keywords;
682 | if (current_company) {
683 | requestData.current_company =
684 | typeof current_company === "string" && current_company.includes("company:")
685 | ? [{ type: "company", value: current_company.replace("company:", "") }]
686 | : current_company;
687 | }
688 | if (past_company) {
689 | requestData.past_company =
690 | typeof past_company === "string" && past_company.includes("company:")
691 | ? [{ type: "company", value: past_company.replace("company:", "") }]
692 | : past_company;
693 | }
694 | if (location) {
695 | requestData.location =
696 | typeof location === "string" && location.includes("geo:")
697 | ? [{ type: "geo", value: location.replace("geo:", "") }]
698 | : location;
699 | }
700 | if (industry) {
701 | requestData.industry =
702 | typeof industry === "string" && industry.includes("industry:")
703 | ? [{ type: "industry", value: industry.replace("industry:", "") }]
704 | : industry;
705 | }
706 | if (education) {
707 | requestData.education =
708 | typeof education === "string" && education.includes("fsd_company:")
709 | ? [{ type: "fsd_company", value: education.replace("fsd_company:", "") }]
710 | : education;
711 | }
712 | log("Starting LinkedIn users search with:", JSON.stringify(requestData));
713 | try {
714 | const response = await makeRequest(API_CONFIG.ENDPOINTS.SEARCH_USERS, requestData);
715 | log(`Search complete, found ${response.length} results`);
716 | return {
717 | content: [
718 | {
719 | type: "text",
720 | mimeType: "application/json",
721 | text: JSON.stringify(response, null, 2)
722 | }
723 | ]
724 | };
725 | } catch (error) {
726 | log("LinkedIn search error:", error);
727 | return {
728 | content: [
729 | {
730 | type: "text",
731 | mimeType: "text/plain",
732 | text: `LinkedIn search API error: ${formatError(error)}`
733 | }
734 | ],
735 | isError: true
736 | };
737 | }
738 | }
739 |
740 | case "get_linkedin_profile": {
741 | if (!isValidLinkedinUserProfileArgs(args)) {
742 | throw new McpError(ErrorCode.InvalidParams, "Invalid LinkedIn profile arguments");
743 | }
744 | const { user, with_experience = true, with_education = true, with_skills = true } = args as LinkedinUserProfileArgs;
745 | const requestData = { timeout: 300, user, with_experience, with_education, with_skills };
746 | log("Starting LinkedIn profile lookup for:", user);
747 | try {
748 | const response = await makeRequest(API_CONFIG.ENDPOINTS.USER_PROFILE, requestData);
749 | return {
750 | content: [
751 | {
752 | type: "text",
753 | mimeType: "application/json",
754 | text: JSON.stringify(response, null, 2)
755 | }
756 | ]
757 | };
758 | } catch (error) {
759 | log("LinkedIn profile lookup error:", error);
760 | return {
761 | content: [
762 | {
763 | type: "text",
764 | mimeType: "text/plain",
765 | text: `LinkedIn API error: ${formatError(error)}`
766 | }
767 | ],
768 | isError: true
769 | };
770 | }
771 | }
772 |
773 | case "get_linkedin_email_user": {
774 | if (!isValidLinkedinEmailUserArgs(args)) {
775 | throw new McpError(ErrorCode.InvalidParams, "Invalid email parameter");
776 | }
777 | const { email, count = 5, timeout = 300 } = args as LinkedinEmailUserArgs;
778 | const requestData = { timeout, email, count };
779 | log("Starting LinkedIn email lookup for:", email);
780 | try {
781 | const response = await makeRequest(API_CONFIG.ENDPOINTS.LINKEDIN_EMAIL, requestData);
782 | return {
783 | content: [
784 | {
785 | type: "text",
786 | mimeType: "application/json",
787 | text: JSON.stringify(response, null, 2)
788 | }
789 | ]
790 | };
791 | } catch (error) {
792 | log("LinkedIn email lookup error:", error);
793 | return {
794 | content: [
795 | {
796 | type: "text",
797 | mimeType: "text/plain",
798 | text: `LinkedIn email API error: ${formatError(error)}`
799 | }
800 | ],
801 | isError: true
802 | };
803 | }
804 | }
805 |
806 | case "get_linkedin_user_posts": {
807 | if (!isValidLinkedinUserPostsArgs(args)) {
808 | throw new McpError(ErrorCode.InvalidParams, "Invalid user posts arguments");
809 | }
810 | const { urn, count = 10, timeout = 300 } = args as LinkedinUserPostsArgs;
811 | const normalizedURN = normalizeUserURN(urn);
812 | if (!isValidUserURN(normalizedURN)) {
813 | throw new McpError(ErrorCode.InvalidParams, "Invalid URN format. Must start with 'fsd_profile:'");
814 | }
815 | log("Starting LinkedIn user posts lookup for urn:", normalizedURN);
816 | const requestData = { timeout, urn: normalizedURN, count };
817 | try {
818 | const response = await makeRequest(API_CONFIG.ENDPOINTS.LINKEDIN_USER_POSTS, requestData);
819 | return {
820 | content: [
821 | {
822 | type: "text",
823 | mimeType: "application/json",
824 | text: JSON.stringify(response, null, 2)
825 | }
826 | ]
827 | };
828 | } catch (error) {
829 | log("LinkedIn user posts lookup error:", error);
830 | return {
831 | content: [
832 | {
833 | type: "text",
834 | mimeType: "text/plain",
835 | text: `LinkedIn user posts API error: ${formatError(error)}`
836 | }
837 | ],
838 | isError: true
839 | };
840 | }
841 | }
842 |
843 | case "get_linkedin_user_reactions": {
844 | if (!isValidLinkedinUserReactionsArgs(args)) {
845 | throw new McpError(ErrorCode.InvalidParams, "Invalid user reactions arguments");
846 | }
847 | const { urn, count = 10, timeout = 300 } = args as LinkedinUserReactionsArgs;
848 | const normalizedURN = normalizeUserURN(urn);
849 | if (!isValidUserURN(normalizedURN)) {
850 | throw new McpError(ErrorCode.InvalidParams, "Invalid URN format. Must start with 'fsd_profile:'");
851 | }
852 | log("Starting LinkedIn user reactions lookup for urn:", normalizedURN);
853 | const requestData = { timeout, urn: normalizedURN, count };
854 | try {
855 | const response = await makeRequest(API_CONFIG.ENDPOINTS.LINKEDIN_USER_REACTIONS, requestData);
856 | return {
857 | content: [
858 | {
859 | type: "text",
860 | mimeType: "application/json",
861 | text: JSON.stringify(response, null, 2)
862 | }
863 | ]
864 | };
865 | } catch (error) {
866 | log("LinkedIn user reactions lookup error:", error);
867 | return {
868 | content: [
869 | {
870 | type: "text",
871 | mimeType: "text/plain",
872 | text: `LinkedIn user reactions API error: ${formatError(error)}`
873 | }
874 | ],
875 | isError: true
876 | };
877 | }
878 | }
879 |
880 | case "get_linkedin_chat_messages": {
881 | if (!isValidLinkedinChatMessagesArgs(args)) {
882 | throw new McpError(ErrorCode.InvalidParams, "Invalid chat messages arguments");
883 | }
884 | const { user, company, count = 20, timeout = 300 } = args as LinkedinChatMessagesArgs;
885 | const normalizedUser = normalizeUserURN(user);
886 | if (!isValidUserURN(normalizedUser)) {
887 | throw new McpError(ErrorCode.InvalidParams, "Invalid URN format. Must start with 'fsd_profile:'");
888 | }
889 | const requestData: any = { timeout, user: normalizedUser, count, account_id: ACCOUNT_ID };
890 | if (company) requestData.company = company;
891 | log("Starting LinkedIn chat messages lookup for user:", normalizedUser);
892 | try {
893 | // Changed from GET to using default POST
894 | const response = await makeRequest(API_CONFIG.ENDPOINTS.CHAT_MESSAGES, requestData);
895 | return {
896 | content: [
897 | {
898 | type: "text",
899 | mimeType: "application/json",
900 | text: JSON.stringify(response, null, 2)
901 | }
902 | ]
903 | };
904 | } catch (error) {
905 | log("LinkedIn chat messages lookup error:", error);
906 | return {
907 | content: [
908 | {
909 | type: "text",
910 | mimeType: "text/plain",
911 | text: `LinkedIn chat messages API error: ${formatError(error)}`
912 | }
913 | ],
914 | isError: true
915 | };
916 | }
917 | }
918 |
919 | case "send_linkedin_chat_message": {
920 | if (!isValidSendLinkedinChatMessageArgs(args)) {
921 | throw new McpError(ErrorCode.InvalidParams, "Invalid parameters for sending chat message");
922 | }
923 | const { user, company, text, timeout = 300 } = args as SendLinkedinChatMessageArgs;
924 | const normalizedUser = normalizeUserURN(user);
925 | if (!isValidUserURN(normalizedUser)) {
926 | throw new McpError(ErrorCode.InvalidParams, "Invalid URN format. Must start with 'fsd_profile:'");
927 | }
928 | const requestData: any = { timeout, user: normalizedUser, text, account_id: ACCOUNT_ID };
929 | if (company) requestData.company = company;
930 | log("Starting LinkedIn send chat message for user:", normalizedUser);
931 | try {
932 | const response = await makeRequest(API_CONFIG.ENDPOINTS.CHAT_MESSAGE, requestData, "POST");
933 | return {
934 | content: [
935 | {
936 | type: "text",
937 | mimeType: "application/json",
938 | text: JSON.stringify(response, null, 2)
939 | }
940 | ]
941 | };
942 | } catch (error) {
943 | log("LinkedIn send chat message error:", error);
944 | return {
945 | content: [
946 | {
947 | type: "text",
948 | mimeType: "text/plain",
949 | text: `LinkedIn send chat message API error: ${formatError(error)}`
950 | }
951 | ],
952 | isError: true
953 | };
954 | }
955 | }
956 |
957 | case "send_linkedin_connection": {
958 | if (!isValidSendLinkedinConnectionArgs(args)) {
959 | throw new McpError(ErrorCode.InvalidParams, "Invalid parameters for connection request");
960 | }
961 | const { user, timeout = 300 } = args as SendLinkedinConnectionArgs;
962 | const normalizedUser = normalizeUserURN(user);
963 | if (!isValidUserURN(normalizedUser)) {
964 | throw new McpError(ErrorCode.InvalidParams, "Invalid URN format. Must start with 'fsd_profile:'");
965 | }
966 | const requestData = { timeout, user: normalizedUser, account_id: ACCOUNT_ID };
967 | log("Sending LinkedIn connection request to user:", normalizedUser);
968 | try {
969 | const response = await makeRequest(API_CONFIG.ENDPOINTS.USER_CONNECTION, requestData, "POST");
970 | return {
971 | content: [
972 | {
973 | type: "text",
974 | mimeType: "application/json",
975 | text: JSON.stringify(response, null, 2)
976 | }
977 | ]
978 | };
979 | } catch (error) {
980 | log("LinkedIn connection request error:", error);
981 | return {
982 | content: [
983 | {
984 | type: "text",
985 | mimeType: "text/plain",
986 | text: `LinkedIn connection request API error: ${formatError(error)}`
987 | }
988 | ],
989 | isError: true
990 | };
991 | }
992 | }
993 |
994 | case "send_linkedin_post_comment": {
995 | if (!isValidSendLinkedinPostCommentArgs(args)) {
996 | throw new McpError(ErrorCode.InvalidParams, "Invalid parameters for commenting on a post");
997 | }
998 | const { text, urn, timeout = 300 } = args as SendLinkedinPostCommentArgs;
999 | const isActivityOrComment = urn.includes("activity:") || urn.includes("comment:");
1000 | if (!isActivityOrComment) {
1001 | throw new McpError(ErrorCode.InvalidParams, "URN must be for an activity or comment");
1002 | }
1003 | let urnObj;
1004 | if (urn.startsWith("activity:")) {
1005 | urnObj = { type: "activity", value: urn.replace("activity:", "") };
1006 | } else if (urn.startsWith("comment:")) {
1007 | urnObj = { type: "comment", value: urn.replace("comment:", "") };
1008 | } else {
1009 | urnObj = urn;
1010 | }
1011 | const requestData = {
1012 | timeout,
1013 | text,
1014 | urn: urnObj,
1015 | account_id: ACCOUNT_ID
1016 | };
1017 | log(`Creating LinkedIn comment on ${urn}`);
1018 | try {
1019 | const response = await makeRequest(API_CONFIG.ENDPOINTS.POST_COMMENT, requestData, "POST");
1020 | return {
1021 | content: [
1022 | {
1023 | type: "text",
1024 | mimeType: "application/json",
1025 | text: JSON.stringify(response, null, 2)
1026 | }
1027 | ]
1028 | };
1029 | } catch (error) {
1030 | log("LinkedIn comment creation error:", error);
1031 | return {
1032 | content: [
1033 | {
1034 | type: "text",
1035 | mimeType: "text/plain",
1036 | text: `LinkedIn comment API error: ${formatError(error)}`
1037 | }
1038 | ],
1039 | isError: true
1040 | };
1041 | }
1042 | }
1043 |
1044 | case "send_linkedin_post": {
1045 | if (!isValidSendLinkedinPostArgs(args)) {
1046 | throw new McpError(ErrorCode.InvalidParams, "Invalid parameters for creating a LinkedIn post");
1047 | }
1048 | const {
1049 | text,
1050 | visibility = "ANYONE",
1051 | comment_scope = "ALL",
1052 | timeout = 300
1053 | } = args as SendLinkedinPostArgs;
1054 |
1055 | const requestData = {
1056 | text,
1057 | visibility,
1058 | comment_scope,
1059 | timeout,
1060 | account_id: ACCOUNT_ID
1061 | };
1062 |
1063 | log("Creating LinkedIn post with text:", text.substring(0, 50) + (text.length > 50 ? "..." : ""));
1064 | try {
1065 | const response = await makeRequest(API_CONFIG.ENDPOINTS.LINKEDIN_POST, requestData, "POST");
1066 | return {
1067 | content: [
1068 | {
1069 | type: "text",
1070 | mimeType: "application/json",
1071 | text: JSON.stringify(response, null, 2)
1072 | }
1073 | ]
1074 | };
1075 | } catch (error) {
1076 | log("LinkedIn post creation error:", error);
1077 | return {
1078 | content: [
1079 | {
1080 | type: "text",
1081 | mimeType: "text/plain",
1082 | text: `LinkedIn post creation API error: ${formatError(error)}`
1083 | }
1084 | ],
1085 | isError: true
1086 | };
1087 | }
1088 | }
1089 |
1090 | case "get_linkedin_user_connections": {
1091 | if (!isValidGetLinkedinUserConnectionsArgs(args)) {
1092 | throw new McpError(ErrorCode.InvalidParams, "Invalid user connections arguments");
1093 | }
1094 | const { connected_after, count = 20, timeout = 300 } = args as GetLinkedinUserConnectionsArgs;
1095 | const requestData: {
1096 | timeout: number;
1097 | account_id: string;
1098 | connected_after?: number;
1099 | count?: number;
1100 | } = {
1101 | timeout: Number(timeout),
1102 | account_id: ACCOUNT_ID!
1103 | };
1104 | if (connected_after != null) {
1105 | requestData.connected_after = Number(connected_after);
1106 | }
1107 | if (count != null) {
1108 | requestData.count = Number(count);
1109 | }
1110 | log("Starting LinkedIn user connections lookup");
1111 | try {
1112 | // Changed from GET to using default POST
1113 | const response = await makeRequest(API_CONFIG.ENDPOINTS.USER_CONNECTIONS, requestData);
1114 | return {
1115 | content: [
1116 | {
1117 | type: "text",
1118 | mimeType: "application/json",
1119 | text: JSON.stringify(response, null, 2)
1120 | }
1121 | ]
1122 | };
1123 | } catch (error) {
1124 | log("LinkedIn user connections lookup error:", error);
1125 | return {
1126 | content: [
1127 | {
1128 | type: "text",
1129 | mimeType: "text/plain",
1130 | text: `LinkedIn user connections API error: ${formatError(error)}`
1131 | }
1132 | ],
1133 | isError: true
1134 | };
1135 | }
1136 | }
1137 |
1138 | case "get_linkedin_post_reposts": {
1139 | if (!isValidGetLinkedinPostRepostsArgs(args)) {
1140 | throw new McpError(ErrorCode.InvalidParams, "Invalid post reposts arguments");
1141 | }
1142 | const { urn, count = 10, timeout = 300 } = args as GetLinkedinPostRepostsArgs;
1143 | const requestData = {
1144 | timeout: Number(timeout),
1145 | urn,
1146 | count: Number(count)
1147 | };
1148 | log(`Starting LinkedIn post reposts lookup for: ${urn}`);
1149 | try {
1150 | const response = await makeRequest(API_CONFIG.ENDPOINTS.LINKEDIN_POST_REPOSTS, requestData);
1151 | return {
1152 | content: [
1153 | {
1154 | type: "text",
1155 | mimeType: "application/json",
1156 | text: JSON.stringify(response, null, 2)
1157 | }
1158 | ]
1159 | };
1160 | } catch (error) {
1161 | log("LinkedIn post reposts lookup error:", error);
1162 | return {
1163 | content: [
1164 | {
1165 | type: "text",
1166 | mimeType: "text/plain",
1167 | text: `LinkedIn post reposts API error: ${formatError(error)}`
1168 | }
1169 | ],
1170 | isError: true
1171 | };
1172 | }
1173 | }
1174 |
1175 | case "get_linkedin_post_comments": {
1176 | if (!isValidGetLinkedinPostCommentsArgs(args)) {
1177 | throw new McpError(ErrorCode.InvalidParams, "Invalid post comments arguments");
1178 | }
1179 | const { urn, sort = "relevance", count = 10, timeout = 300 } = args as GetLinkedinPostCommentsArgs;
1180 | const requestData = {
1181 | timeout: Number(timeout),
1182 | urn,
1183 | sort,
1184 | count: Number(count)
1185 | };
1186 | log(`Starting LinkedIn post comments lookup for: ${urn}`);
1187 | try {
1188 | const response = await makeRequest(API_CONFIG.ENDPOINTS.LINKEDIN_POST_COMMENTS, requestData);
1189 | return {
1190 | content: [
1191 | {
1192 | type: "text",
1193 | mimeType: "application/json",
1194 | text: JSON.stringify(response, null, 2)
1195 | }
1196 | ]
1197 | };
1198 | } catch (error) {
1199 | log("LinkedIn post comments lookup error:", error);
1200 | return {
1201 | content: [
1202 | {
1203 | type: "text",
1204 | mimeType: "text/plain",
1205 | text: `LinkedIn post comments API error: ${formatError(error)}`
1206 | }
1207 | ],
1208 | isError: true
1209 | };
1210 | }
1211 | }
1212 |
1213 | case "get_linkedin_google_company": {
1214 | if (!isValidGetLinkedinGoogleCompanyArgs(args)) {
1215 | throw new McpError(ErrorCode.InvalidParams, "Invalid Google company search arguments");
1216 | }
1217 | const { keywords, with_urn = false, count_per_keyword = 1, timeout = 300 } = args as GetLinkedinGoogleCompanyArgs;
1218 | const requestData = {
1219 | timeout: Number(timeout),
1220 | keywords,
1221 | with_urn: Boolean(with_urn),
1222 | count_per_keyword: Number(count_per_keyword)
1223 | };
1224 | log(`Starting LinkedIn Google company search for keywords: ${keywords.join(', ')}`);
1225 | try {
1226 | const response = await makeRequest(API_CONFIG.ENDPOINTS.LINKEDIN_GOOGLE_COMPANY, requestData);
1227 | return {
1228 | content: [
1229 | {
1230 | type: "text",
1231 | mimeType: "application/json",
1232 | text: JSON.stringify(response, null, 2)
1233 | }
1234 | ]
1235 | };
1236 | } catch (error) {
1237 | log("LinkedIn Google company search error:", error);
1238 | return {
1239 | content: [
1240 | {
1241 | type: "text",
1242 | mimeType: "text/plain",
1243 | text: `LinkedIn Google company search API error: ${formatError(error)}`
1244 | }
1245 | ],
1246 | isError: true
1247 | };
1248 | }
1249 | }
1250 |
1251 | case "get_linkedin_company": {
1252 | if (!isValidGetLinkedinCompanyArgs(args)) {
1253 | throw new McpError(ErrorCode.InvalidParams, "Invalid company arguments");
1254 | }
1255 | const { company, timeout = 300 } = args as GetLinkedinCompanyArgs;
1256 | const requestData = {
1257 | timeout: Number(timeout),
1258 | company
1259 | };
1260 | log(`Starting LinkedIn company lookup for: ${company}`);
1261 | try {
1262 | const response = await makeRequest(API_CONFIG.ENDPOINTS.LINKEDIN_COMPANY, requestData);
1263 | return {
1264 | content: [
1265 | {
1266 | type: "text",
1267 | mimeType: "application/json",
1268 | text: JSON.stringify(response, null, 2)
1269 | }
1270 | ]
1271 | };
1272 | } catch (error) {
1273 | log("LinkedIn company lookup error:", error);
1274 | return {
1275 | content: [
1276 | {
1277 | type: "text",
1278 | mimeType: "text/plain",
1279 | text: `LinkedIn company API error: ${formatError(error)}`
1280 | }
1281 | ],
1282 | isError: true
1283 | };
1284 | }
1285 | }
1286 |
1287 | case "get_linkedin_company_employees": {
1288 | if (!isValidGetLinkedinCompanyEmployeesArgs(args)) {
1289 | throw new McpError(ErrorCode.InvalidParams, "Invalid company employees arguments");
1290 | }
1291 | const { companies, keywords, first_name, last_name, count = 10, timeout = 300 } = args as GetLinkedinCompanyEmployeesArgs;
1292 | const requestData: {
1293 | timeout: number;
1294 | companies: string[];
1295 | keywords?: string;
1296 | first_name?: string;
1297 | last_name?: string;
1298 | count: number;
1299 | } = {
1300 | timeout: Number(timeout),
1301 | companies,
1302 | count: Number(count)
1303 | };
1304 | if (keywords != null && typeof keywords === 'string') {
1305 | requestData.keywords = keywords;
1306 | }
1307 | if (first_name != null && typeof first_name === 'string') {
1308 | requestData.first_name = first_name;
1309 | }
1310 | if (last_name != null && typeof last_name === 'string') {
1311 | requestData.last_name = last_name;
1312 | }
1313 | log(`Starting LinkedIn company employees lookup for companies: ${companies.join(', ')}`);
1314 | try {
1315 | const response = await makeRequest(API_CONFIG.ENDPOINTS.LINKEDIN_COMPANY_EMPLOYEES, requestData);
1316 | return {
1317 | content: [
1318 | {
1319 | type: "text",
1320 | mimeType: "application/json",
1321 | text: JSON.stringify(response, null, 2)
1322 | }
1323 | ]
1324 | };
1325 | } catch (error) {
1326 | log("LinkedIn company employees lookup error:", error);
1327 | return {
1328 | content: [
1329 | {
1330 | type: "text",
1331 | mimeType: "text/plain",
1332 | text: `LinkedIn company employees API error: ${formatError(error)}`
1333 | }
1334 | ],
1335 | isError: true
1336 | };
1337 | }
1338 | }
1339 |
1340 | case "linkedin_sn_search_users": {
1341 | if (!isValidLinkedinSalesNavigatorSearchUsersArgs(args)) {
1342 | throw new McpError(ErrorCode.InvalidParams, "Invalid LinkedIn Sales Navigator search arguments");
1343 | }
1344 |
1345 | const {
1346 | keywords,
1347 | first_names,
1348 | last_names,
1349 | current_titles,
1350 | location,
1351 | education,
1352 | languages,
1353 | past_titles,
1354 | functions,
1355 | levels,
1356 | years_in_the_current_company,
1357 | years_in_the_current_position,
1358 | company_sizes,
1359 | company_types,
1360 | company_locations,
1361 | current_companies,
1362 | past_companies,
1363 | industry,
1364 | count,
1365 | timeout = 300
1366 | } = args as LinkedinSalesNavigatorSearchUsersArgs;
1367 |
1368 | const requestData: Record = {
1369 | count,
1370 | timeout
1371 | };
1372 |
1373 | if (keywords) requestData.keywords = keywords;
1374 | if (first_names) requestData.first_names = first_names;
1375 | if (last_names) requestData.last_names = last_names;
1376 | if (current_titles) requestData.current_titles = current_titles;
1377 |
1378 | if (location) {
1379 | requestData.location = typeof location === "string" && location.includes("geo:")
1380 | ? [{ type: "geo", value: location.replace("geo:", "") }]
1381 | : location;
1382 | }
1383 |
1384 | if (education) {
1385 | requestData.education = typeof education === "string" && education.includes("company:")
1386 | ? [{ type: "company", value: education.replace("company:", "") }]
1387 | : education;
1388 | }
1389 |
1390 | if (languages) requestData.languages = languages;
1391 | if (past_titles) requestData.past_titles = past_titles;
1392 | if (functions) requestData.functions = functions;
1393 | if (levels) requestData.levels = levels;
1394 | if (years_in_the_current_company) requestData.years_in_the_current_company = years_in_the_current_company;
1395 | if (years_in_the_current_position) requestData.years_in_the_current_position = years_in_the_current_position;
1396 | if (company_sizes) requestData.company_sizes = company_sizes;
1397 | if (company_types) requestData.company_types = company_types;
1398 |
1399 | if (company_locations) {
1400 | requestData.company_locations = typeof company_locations === "string" && company_locations.includes("geo:")
1401 | ? [{ type: "geo", value: company_locations.replace("geo:", "") }]
1402 | : company_locations;
1403 | }
1404 |
1405 | if (current_companies) {
1406 | requestData.current_companies = typeof current_companies === "string" && current_companies.includes("company:")
1407 | ? [{ type: "company", value: current_companies.replace("company:", "") }]
1408 | : current_companies;
1409 | }
1410 |
1411 | if (past_companies) {
1412 | requestData.past_companies = typeof past_companies === "string" && past_companies.includes("company:")
1413 | ? [{ type: "company", value: past_companies.replace("company:", "") }]
1414 | : past_companies;
1415 | }
1416 |
1417 | if (industry) {
1418 | requestData.industry = typeof industry === "string" && industry.includes("industry:")
1419 | ? [{ type: "industry", value: industry.replace("industry:", "") }]
1420 | : industry;
1421 | }
1422 |
1423 | log("Starting LinkedIn Sales Navigator users search with filters");
1424 | try {
1425 | const response = await makeRequest(API_CONFIG.ENDPOINTS.LINKEDIN_SN_SEARCH_USERS, requestData);
1426 | log(`Search complete, found ${response.length} results`);
1427 | return {
1428 | content: [
1429 | {
1430 | type: "text",
1431 | mimeType: "application/json",
1432 | text: JSON.stringify(response, null, 2)
1433 | }
1434 | ]
1435 | };
1436 | } catch (error) {
1437 | log("LinkedIn Sales Navigator search error:", error);
1438 | return {
1439 | content: [
1440 | {
1441 | type: "text",
1442 | mimeType: "text/plain",
1443 | text: `LinkedIn Sales Navigator search API error: ${formatError(error)}`
1444 | }
1445 | ],
1446 | isError: true
1447 | };
1448 | }
1449 | }
1450 |
1451 | case "get_linkedin_conversations": {
1452 | if (!isValidLinkedinManagementConversationsArgs(args)) {
1453 | throw new McpError(ErrorCode.InvalidParams, "Invalid conversations arguments");
1454 | }
1455 | const { connected_after, count = 20, timeout = 300 } = args as LinkedinManagementConversationsPayload;
1456 | const requestData: {
1457 | timeout: number;
1458 | account_id: string;
1459 | connected_after?: number;
1460 | count?: number;
1461 | } = {
1462 | timeout: Number(timeout),
1463 | account_id: ACCOUNT_ID!
1464 | };
1465 | if (connected_after != null) {
1466 | requestData.connected_after = Number(connected_after);
1467 | }
1468 | if (count != null) {
1469 | requestData.count = Number(count);
1470 | }
1471 | log("Starting LinkedIn conversations lookup");
1472 | try {
1473 | // Changed from GET to using default POST
1474 | const response = await makeRequest(API_CONFIG.ENDPOINTS.CONVERSATIONS, requestData);
1475 | return {
1476 | content: [
1477 | {
1478 | type: "text",
1479 | mimeType: "application/json",
1480 | text: JSON.stringify(response, null, 2)
1481 | }
1482 | ]
1483 | };
1484 | } catch (error) {
1485 | log("LinkedIn conversations lookup error:", error);
1486 | return {
1487 | content: [
1488 | {
1489 | type: "text",
1490 | mimeType: "text/plain",
1491 | text: `LinkedIn conversations API error: ${formatError(error)}`
1492 | }
1493 | ],
1494 | isError: true
1495 | };
1496 | }
1497 | }
1498 |
1499 | case "google_search": {
1500 | if (!isValidGoogleSearchPayload(args)) {
1501 | throw new McpError(ErrorCode.InvalidParams, "Invalid Google search arguments");
1502 | }
1503 | const { query, count = 10, timeout = 300 } = args as GoogleSearchPayload;
1504 | const requestData = {
1505 | timeout,
1506 | query,
1507 | count: Math.min(Math.max(1, count), 20) // Ensure count is between 1 and 20
1508 | };
1509 | log(`Starting Google search for: ${query}`);
1510 | try {
1511 | const response = await makeRequest(API_CONFIG.ENDPOINTS.GOOGLE_SEARCH, requestData);
1512 | return {
1513 | content: [
1514 | {
1515 | type: "text",
1516 | mimeType: "application/json",
1517 | text: JSON.stringify(response, null, 2)
1518 | }
1519 | ]
1520 | };
1521 | } catch (error) {
1522 | log("Google search error:", error);
1523 | return {
1524 | content: [
1525 | {
1526 | type: "text",
1527 | mimeType: "text/plain",
1528 | text: `Google search API error: ${formatError(error)}`
1529 | }
1530 | ],
1531 | isError: true
1532 | };
1533 | }
1534 | }
1535 |
1536 | default:
1537 | throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
1538 | }
1539 | } catch (error) {
1540 | log("Tool error:", error);
1541 | return {
1542 | content: [
1543 | {
1544 | type: "text",
1545 | mimeType: "text/plain",
1546 | text: `API error: ${formatError(error)}`
1547 | }
1548 | ],
1549 | isError: true
1550 | };
1551 | }
1552 | });
1553 |
1554 | async function runServer() {
1555 | const transport = new StdioServerTransport();
1556 | log("Starting HDW MCP Server...");
1557 |
1558 | process.on("uncaughtException", (error) => {
1559 | log("Uncaught Exception:", error);
1560 | });
1561 | process.on("unhandledRejection", (reason, promise) => {
1562 | log("Unhandled Rejection at:", promise, "reason:", reason);
1563 | });
1564 |
1565 | await server.connect(transport);
1566 | log("HDW MCP Server running on stdio");
1567 | }
1568 |
1569 | runServer().catch((error) => {
1570 | log("Fatal error running server:", error);
1571 | process.exit(1);
1572 | });
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | // types.ts
2 | export interface LinkedinSearchUsersArgs {
3 | keywords?: string;
4 | first_name?: string;
5 | last_name?: string;
6 | title?: string;
7 | company_keywords?: string;
8 | school_keywords?: string;
9 | current_company?: string;
10 | past_company?: string;
11 | location?: string;
12 | industry?: string;
13 | education?: string;
14 | count?: number;
15 | timeout?: number;
16 | }
17 |
18 | export interface LinkedinUserProfileArgs {
19 | user: string;
20 | with_experience?: boolean;
21 | with_education?: boolean;
22 | with_skills?: boolean;
23 | }
24 |
25 | export interface LinkedinEmailUserArgs {
26 | email: string;
27 | count?: number;
28 | timeout?: number;
29 | }
30 |
31 | export interface LinkedinUserPostsArgs {
32 | urn: string;
33 | count?: number;
34 | timeout?: number;
35 | }
36 |
37 | export interface LinkedinUserReactionsArgs {
38 | urn: string;
39 | count?: number;
40 | timeout?: number;
41 | }
42 |
43 | export interface LinkedinChatMessagesArgs {
44 | user: string;
45 | company?: string;
46 | count?: number;
47 | timeout?: number;
48 | }
49 |
50 | export interface SendLinkedinChatMessageArgs {
51 | user: string;
52 | company?: string;
53 | text: string;
54 | timeout?: number;
55 | }
56 |
57 | export interface SendLinkedinConnectionArgs {
58 | user: string;
59 | timeout?: number;
60 | }
61 |
62 | export interface SendLinkedinPostCommentArgs {
63 | text: string;
64 | urn: string;
65 | timeout?: number;
66 | }
67 |
68 | export interface GetLinkedinUserConnectionsArgs {
69 | connected_after?: number;
70 | count?: number;
71 | timeout?: number;
72 | }
73 |
74 | export interface GetLinkedinPostRepostsArgs {
75 | urn: string;
76 | count?: number;
77 | timeout?: number;
78 | }
79 |
80 | export interface GetLinkedinPostCommentsArgs {
81 | urn: string;
82 | sort?: "relevance" | "recent";
83 | count?: number;
84 | timeout?: number;
85 | }
86 |
87 | export interface GetLinkedinGoogleCompanyArgs {
88 | keywords: string[];
89 | with_urn?: boolean;
90 | count_per_keyword?: number;
91 | timeout?: number;
92 | }
93 |
94 | export interface GetLinkedinCompanyArgs {
95 | company: string;
96 | timeout?: number;
97 | }
98 |
99 | export interface GetLinkedinCompanyEmployeesArgs {
100 | companies: string[];
101 | keywords?: string;
102 | first_name?: string;
103 | last_name?: string;
104 | count?: number;
105 | timeout?: number;
106 | }
107 |
108 | export interface SendLinkedinPostArgs {
109 | text: string;
110 | visibility?: "ANYONE" | "CONNECTIONS_ONLY";
111 | comment_scope?: "ALL" | "CONNECTIONS_ONLY" | "NONE";
112 | timeout?: number;
113 | }
114 |
115 | export interface LinkedinSalesNavigatorSearchUsersArgs {
116 | keywords?: string;
117 | first_names?: string[];
118 | last_names?: string[];
119 | current_titles?: string[];
120 | location?: string | string[];
121 | education?: string | string[];
122 | languages?: string[];
123 | past_titles?: string[];
124 | functions?: string[];
125 | levels?: string[];
126 | years_in_the_current_company?: string[];
127 | years_in_the_current_position?: string[];
128 | company_sizes?: string[];
129 | company_types?: string[];
130 | company_locations?: string | string[];
131 | current_companies?: string | string[];
132 | past_companies?: string | string[];
133 | industry?: string | string[];
134 | count: number;
135 | timeout?: number;
136 | }
137 |
138 | export interface LinkedinManagementConversationsPayload {
139 | connected_after?: number;
140 | count?: number;
141 | timeout?: number;
142 | }
143 |
144 | export interface GoogleSearchPayload {
145 | query: string;
146 | count?: number;
147 | timeout?: number;
148 | }
149 |
150 | export function isValidLinkedinSearchUsersArgs(
151 | args: unknown
152 | ): args is LinkedinSearchUsersArgs {
153 | if (typeof args !== "object" || args === null) return false;
154 | const obj = args as Record;
155 |
156 | if (obj.count !== undefined && typeof obj.count !== "number") {
157 | return false;
158 | }
159 |
160 | if (obj.timeout !== undefined && typeof obj.timeout !== "number") {
161 | return false;
162 | }
163 |
164 | const hasAnySearchField =
165 | obj.keywords ||
166 | obj.first_name ||
167 | obj.last_name ||
168 | obj.title ||
169 | obj.company_keywords ||
170 | obj.school_keywords ||
171 | obj.current_company ||
172 | obj.past_company ||
173 | obj.location ||
174 | obj.industry ||
175 | obj.education;
176 |
177 | if (!hasAnySearchField) return false;
178 |
179 | return true;
180 | }
181 |
182 | export function isValidLinkedinUserProfileArgs(
183 | args: unknown
184 | ): args is LinkedinUserProfileArgs {
185 | if (typeof args !== "object" || args === null) return false;
186 | const obj = args as Record;
187 | if (typeof obj.user !== "string" || !obj.user.trim()) return false;
188 |
189 | return true;
190 | }
191 |
192 | export function isValidLinkedinEmailUserArgs(
193 | args: unknown
194 | ): args is LinkedinEmailUserArgs {
195 | if (typeof args !== "object" || args === null) return false;
196 | const obj = args as Record;
197 | if (typeof obj.email !== "string" || !obj.email.trim()) return false;
198 | if (obj.count !== undefined && typeof obj.count !== "number") return false;
199 | if (obj.timeout !== undefined && typeof obj.timeout !== "number") return false;
200 | return true;
201 | }
202 |
203 | export function isValidLinkedinUserPostsArgs(
204 | args: unknown
205 | ): args is LinkedinUserPostsArgs {
206 | if (typeof args !== "object" || args === null) return false;
207 | const obj = args as Record;
208 | if (typeof obj.urn !== "string" || !obj.urn.trim()) return false;
209 | if (obj.count !== undefined && typeof obj.count !== "number") return false;
210 | if (obj.timeout !== undefined && typeof obj.timeout !== "number") return false;
211 | return true;
212 | }
213 |
214 | export function isValidLinkedinUserReactionsArgs(
215 | args: unknown
216 | ): args is LinkedinUserReactionsArgs {
217 | if (typeof args !== "object" || args === null) return false;
218 | const obj = args as Record;
219 | if (typeof obj.urn !== "string" || !obj.urn.trim()) return false;
220 | if (obj.count !== undefined && typeof obj.count !== "number") return false;
221 | if (obj.timeout !== undefined && typeof obj.timeout !== "number") return false;
222 | return true;
223 | }
224 |
225 | export function isValidLinkedinChatMessagesArgs(
226 | args: unknown
227 | ): args is LinkedinChatMessagesArgs {
228 | if (typeof args !== "object" || args === null) return false;
229 | const obj = args as Record;
230 | if (typeof obj.user !== "string" || !obj.user.trim()) return false;
231 | if (obj.company !== undefined && typeof obj.company !== "string") return false;
232 | if (obj.count !== undefined && typeof obj.count !== "number") return false;
233 | if (obj.timeout !== undefined && typeof obj.timeout !== "number") return false;
234 | return true;
235 | }
236 |
237 | export function isValidSendLinkedinChatMessageArgs(
238 | args: unknown
239 | ): args is SendLinkedinChatMessageArgs {
240 | if (typeof args !== "object" || args === null) return false;
241 | const obj = args as Record;
242 | if (typeof obj.user !== "string" || !obj.user.trim()) return false;
243 | if (obj.company !== undefined && typeof obj.company !== "string") return false;
244 | if (typeof obj.text !== "string" || !obj.text.trim()) return false;
245 | if (obj.timeout !== undefined && typeof obj.timeout !== "number") return false;
246 | return true;
247 | }
248 |
249 | export function isValidSendLinkedinConnectionArgs(
250 | args: unknown
251 | ): args is SendLinkedinConnectionArgs {
252 | if (typeof args !== "object" || args === null) return false;
253 | const obj = args as Record;
254 | if (typeof obj.user !== "string" || !obj.user.trim()) return false;
255 | if (obj.timeout !== undefined && typeof obj.timeout !== "number") return false;
256 | return true;
257 | }
258 |
259 | export function isValidSendLinkedinPostCommentArgs(
260 | args: unknown
261 | ): args is SendLinkedinPostCommentArgs {
262 | if (typeof args !== "object" || args === null) return false;
263 | const obj = args as Record;
264 | if (typeof obj.text !== "string" || !obj.text.trim()) return false;
265 | if (typeof obj.urn !== "string" || !obj.urn.trim()) return false;
266 | if (obj.timeout !== undefined && typeof obj.timeout !== "number") return false;
267 | return true;
268 | }
269 |
270 | export function isValidGetLinkedinUserConnectionsArgs(
271 | args: unknown
272 | ): args is GetLinkedinUserConnectionsArgs {
273 | if (typeof args !== "object" || args === null) return false;
274 | const obj = args as Record;
275 | if (obj.connected_after !== undefined && typeof obj.connected_after !== "number") return false;
276 | if (obj.count !== undefined && typeof obj.count !== "number") return false;
277 | if (obj.timeout !== undefined && typeof obj.timeout !== "number") return false;
278 | return true;
279 | }
280 |
281 | export function isValidGetLinkedinPostRepostsArgs(
282 | args: unknown
283 | ): args is GetLinkedinPostRepostsArgs {
284 | if (typeof args !== "object" || args === null) return false;
285 | const obj = args as Record;
286 | if (typeof obj.urn !== "string" || !obj.urn.includes("activity:")) return false;
287 | if (obj.count !== undefined && typeof obj.count !== "number") return false;
288 | if (obj.timeout !== undefined && typeof obj.timeout !== "number") return false;
289 | return true;
290 | }
291 |
292 | export function isValidGetLinkedinPostCommentsArgs(
293 | args: unknown
294 | ): args is GetLinkedinPostCommentsArgs {
295 | if (typeof args !== "object" || args === null) return false;
296 | const obj = args as Record;
297 | if (typeof obj.urn !== "string" || !obj.urn.includes("activity:")) return false;
298 | if (obj.sort !== undefined && obj.sort !== "relevance" && obj.sort !== "recent") return false;
299 | if (obj.count !== undefined && typeof obj.count !== "number") return false;
300 | if (obj.timeout !== undefined && typeof obj.timeout !== "number") return false;
301 | return true;
302 | }
303 |
304 | export function isValidGetLinkedinGoogleCompanyArgs(
305 | args: unknown
306 | ): args is GetLinkedinGoogleCompanyArgs {
307 | if (typeof args !== "object" || args === null) return false;
308 | const obj = args as Record;
309 | if (!Array.isArray(obj.keywords) || obj.keywords.length === 0) return false;
310 | if (obj.with_urn !== undefined && typeof obj.with_urn !== "boolean") return false;
311 | if (
312 | obj.count_per_keyword !== undefined &&
313 | (typeof obj.count_per_keyword !== "number" ||
314 | obj.count_per_keyword < 1 ||
315 | obj.count_per_keyword > 10)
316 | ) {
317 | return false;
318 | }
319 | if (obj.timeout !== undefined && typeof obj.timeout !== "number") return false;
320 | return true;
321 | }
322 |
323 | export function isValidGetLinkedinCompanyArgs(
324 | args: unknown
325 | ): args is GetLinkedinCompanyArgs {
326 | if (typeof args !== "object" || args === null) return false;
327 | const obj = args as Record;
328 | if (typeof obj.company !== "string" || !obj.company.trim()) return false;
329 | if (obj.timeout !== undefined && typeof obj.timeout !== "number") return false;
330 | return true;
331 | }
332 |
333 | export function isValidGetLinkedinCompanyEmployeesArgs(
334 | args: unknown
335 | ): args is GetLinkedinCompanyEmployeesArgs {
336 | if (typeof args !== "object" || args === null) return false;
337 | const obj = args as Record;
338 |
339 | // companies (обязательный массив строк)
340 | if (!Array.isArray(obj.companies) || obj.companies.length === 0) return false;
341 | for (const c of obj.companies) {
342 | if (typeof c !== "string") return false;
343 | }
344 |
345 | if (obj.keywords !== undefined && typeof obj.keywords !== "string") return false;
346 | if (obj.first_name !== undefined && typeof obj.first_name !== "string") return false;
347 | if (obj.last_name !== undefined && typeof obj.last_name !== "string") return false;
348 | if (obj.count !== undefined && typeof obj.count !== "number") return false;
349 | if (obj.timeout !== undefined && typeof obj.timeout !== "number") return false;
350 |
351 | return true;
352 | }
353 |
354 | export function isValidSendLinkedinPostArgs(
355 | args: unknown
356 | ): args is SendLinkedinPostArgs {
357 | if (typeof args !== "object" || args === null) return false;
358 | const obj = args as Record;
359 |
360 | if (typeof obj.text !== "string" || !obj.text.trim()) return false;
361 |
362 | if (obj.visibility !== undefined &&
363 | obj.visibility !== "ANYONE" &&
364 | obj.visibility !== "CONNECTIONS_ONLY") return false;
365 |
366 | if (obj.comment_scope !== undefined &&
367 | obj.comment_scope !== "ALL" &&
368 | obj.comment_scope !== "CONNECTIONS_ONLY" &&
369 | obj.comment_scope !== "NONE") return false;
370 |
371 | if (obj.timeout !== undefined && typeof obj.timeout !== "number") return false;
372 |
373 | return true;
374 | }
375 |
376 | export function isValidLinkedinSalesNavigatorSearchUsersArgs(
377 | args: unknown
378 | ): args is LinkedinSalesNavigatorSearchUsersArgs {
379 | if (typeof args !== "object" || args === null) return false;
380 | const obj = args as Record;
381 |
382 | if (typeof obj.count !== "number" || obj.count <= 0 || obj.count > 2500) return false;
383 |
384 | if (obj.keywords !== undefined && typeof obj.keywords !== "string") return false;
385 |
386 | if (obj.first_names !== undefined) {
387 | if (!Array.isArray(obj.first_names)) return false;
388 | for (const name of obj.first_names) {
389 | if (typeof name !== "string") return false;
390 | }
391 | }
392 |
393 | if (obj.last_names !== undefined) {
394 | if (!Array.isArray(obj.last_names)) return false;
395 | for (const name of obj.last_names) {
396 | if (typeof name !== "string") return false;
397 | }
398 | }
399 |
400 | if (obj.current_titles !== undefined) {
401 | if (!Array.isArray(obj.current_titles)) return false;
402 | for (const title of obj.current_titles) {
403 | if (typeof title !== "string") return false;
404 | }
405 | }
406 |
407 | if (obj.location !== undefined) {
408 | if (typeof obj.location !== "string" && !Array.isArray(obj.location)) return false;
409 | if (Array.isArray(obj.location)) {
410 | for (const loc of obj.location) {
411 | if (typeof loc !== "string") return false;
412 | }
413 | }
414 | }
415 |
416 | if (obj.education !== undefined) {
417 | if (typeof obj.education !== "string" && !Array.isArray(obj.education)) return false;
418 | if (Array.isArray(obj.education)) {
419 | for (const edu of obj.education) {
420 | if (typeof edu !== "string") return false;
421 | }
422 | }
423 | }
424 |
425 | if (obj.languages !== undefined) {
426 | if (!Array.isArray(obj.languages)) return false;
427 | for (const lang of obj.languages) {
428 | if (typeof lang !== "string") return false;
429 | }
430 | }
431 |
432 | if (obj.past_titles !== undefined) {
433 | if (!Array.isArray(obj.past_titles)) return false;
434 | for (const title of obj.past_titles) {
435 | if (typeof title !== "string") return false;
436 | }
437 | }
438 |
439 | if (obj.functions !== undefined) {
440 | if (!Array.isArray(obj.functions)) return false;
441 | for (const func of obj.functions) {
442 | if (typeof func !== "string") return false;
443 | }
444 | }
445 |
446 | if (obj.levels !== undefined) {
447 | if (!Array.isArray(obj.levels)) return false;
448 | for (const level of obj.levels) {
449 | if (typeof level !== "string") return false;
450 | }
451 | }
452 |
453 | if (obj.years_in_the_current_company !== undefined) {
454 | if (!Array.isArray(obj.years_in_the_current_company)) return false;
455 | for (const years of obj.years_in_the_current_company) {
456 | if (typeof years !== "string") return false;
457 | }
458 | }
459 |
460 | if (obj.years_in_the_current_position !== undefined) {
461 | if (!Array.isArray(obj.years_in_the_current_position)) return false;
462 | for (const years of obj.years_in_the_current_position) {
463 | if (typeof years !== "string") return false;
464 | }
465 | }
466 |
467 | if (obj.company_sizes !== undefined) {
468 | if (!Array.isArray(obj.company_sizes)) return false;
469 | for (const size of obj.company_sizes) {
470 | if (typeof size !== "string") return false;
471 | }
472 | }
473 |
474 | if (obj.company_types !== undefined) {
475 | if (!Array.isArray(obj.company_types)) return false;
476 | for (const type of obj.company_types) {
477 | if (typeof type !== "string") return false;
478 | }
479 | }
480 |
481 | if (obj.company_locations !== undefined) {
482 | if (typeof obj.company_locations !== "string" && !Array.isArray(obj.company_locations)) return false;
483 | if (Array.isArray(obj.company_locations)) {
484 | for (const loc of obj.company_locations) {
485 | if (typeof loc !== "string") return false;
486 | }
487 | }
488 | }
489 |
490 | if (obj.current_companies !== undefined) {
491 | if (typeof obj.current_companies !== "string" && !Array.isArray(obj.current_companies)) return false;
492 | if (Array.isArray(obj.current_companies)) {
493 | for (const company of obj.current_companies) {
494 | if (typeof company !== "string") return false;
495 | }
496 | }
497 | }
498 |
499 | if (obj.past_companies !== undefined) {
500 | if (typeof obj.past_companies !== "string" && !Array.isArray(obj.past_companies)) return false;
501 | if (Array.isArray(obj.past_companies)) {
502 | for (const company of obj.past_companies) {
503 | if (typeof company !== "string") return false;
504 | }
505 | }
506 | }
507 |
508 | if (obj.industry !== undefined) {
509 | if (typeof obj.industry !== "string" && !Array.isArray(obj.industry)) return false;
510 | if (Array.isArray(obj.industry)) {
511 | for (const ind of obj.industry) {
512 | if (typeof ind !== "string") return false;
513 | }
514 | }
515 | }
516 |
517 | if (obj.timeout !== undefined && typeof obj.timeout !== "number") return false;
518 |
519 | return true;
520 | }
521 |
522 | export function isValidLinkedinManagementConversationsArgs(
523 | args: unknown
524 | ): args is LinkedinManagementConversationsPayload {
525 | if (typeof args !== "object" || args === null) return false;
526 | const obj = args as Record;
527 | if (obj.connected_after !== undefined && typeof obj.connected_after !== "number") return false;
528 | if (obj.count !== undefined && typeof obj.count !== "number") return false;
529 | if (obj.timeout !== undefined && typeof obj.timeout !== "number") return false;
530 | return true;
531 | }
532 |
533 | export function isValidGoogleSearchPayload(
534 | args: unknown
535 | ): args is GoogleSearchPayload {
536 | if (typeof args !== "object" || args === null) return false;
537 | const obj = args as Record;
538 |
539 | if (typeof obj.query !== "string" || !obj.query.trim()) return false;
540 | if (obj.count !== undefined && (typeof obj.count !== "number" || obj.count <= 0 || obj.count > 20)) return false;
541 | if (obj.timeout !== undefined && (typeof obj.timeout !== "number" || obj.timeout < 20 || obj.timeout > 1500)) return false;
542 |
543 | return true;
544 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "outDir": "./build",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true
12 | },
13 | "include": ["src/**/*"],
14 | "exclude": ["node_modules"]
15 | }
--------------------------------------------------------------------------------