├── .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 | 36 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | [![smithery badge](https://smithery.ai/badge/@horizondatawave/hdw-mcp-server)](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 | } --------------------------------------------------------------------------------