├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── pnpm-lock.yaml ├── src ├── api │ ├── blogs.ts │ ├── courses.ts │ └── resource.ts ├── config │ ├── env.ts │ └── urls.ts ├── index.ts ├── middleware │ └── withError.ts ├── tools │ ├── BlogTool.ts │ ├── CourseTool.ts │ └── ResourceTool.ts └── types │ └── api │ ├── blogs.ts │ ├── courses.ts │ └── resource.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # Base URL for the application 2 | BASE_URL=https://interviewready.io 3 | 4 | # API URL for the application 5 | API_URL=https://api.interviewready.io/api -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Build output 8 | build/ 9 | dist/ 10 | out/ 11 | 12 | # TypeScript cache 13 | *.tsbuildinfo 14 | 15 | # Environment variables 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | # Editor directories and files 23 | .idea/ 24 | .vscode/ 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | 31 | # OS generated files 32 | .DS_Store 33 | .DS_Store? 34 | ._* 35 | .Spotlight-V100 36 | .Trashes 37 | ehthumbs.db 38 | Thumbs.db 39 | 40 | # Coverage directory 41 | coverage/ 42 | 43 | # Logs 44 | logs/ 45 | *.log 46 | 47 | # Temporary files 48 | tmp/ 49 | temp/ 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 InterviewReady 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mcp-server 2 | An MCP server for InterviewReady. 3 | 4 | 1. GET ACTIONS: Exposes APIs to fetch the most relevant content from InterviewReady including blogs, resources and course materials. 5 | 2. UPDATE ACTIONS: Allows adding notes to a user's notepad, and setting google reminders for future classes. 6 | 7 | Please feel free to add / change capabilities of this repo. 8 | 9 | If you have doubts, please post them in discussions. If there are any problems/issues, please create an issue. 10 | 11 | Please self-review any PR before making a contribution. 12 | 13 | The repo is open for change! 14 | 15 | 16 | ## Setup and run the server 17 | 18 | 1. Clone the repo 19 | 2. Run `pnpm install` 20 | 3. Run `pnpm run build` 21 | 22 | ## Setup with Claude Desktop 23 | 24 | 1. Edit the config file for claude desktop `claude_desktop_config.json` 25 | 2. Add the following to the config file: 26 | 27 | ```json 28 | { 29 | "interviewready-mcp-server": { 30 | "command": "node", 31 | "args": [ 32 | "{path-to-repo}/mcp-server/build/index.js" 33 | ] 34 | } 35 | } 36 | ``` 37 | 38 | 39 | ## Setup with Cursor 40 | 41 | 1. Go to Cursor > Settings > Cursor Settings 42 | 2. Go to the `MCP` tab 43 | 3. Add the following to the config file: 44 | 45 | ```json 46 | { 47 | "interviewready-mcp-server": { 48 | "command": "node", 49 | "args": [ 50 | "{path-to-repo}/mcp-server/build/index.js" 51 | ] 52 | } 53 | } 54 | ``` 55 | 4. Use in agent mode. For some reason, MCP is not working in Ask mode. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "bin": { 8 | "mcp-server": "./build/index.js" 9 | }, 10 | "scripts": { 11 | "build": "tsc && chmod 755 build/index.js" 12 | }, 13 | "files": [ 14 | "build" 15 | ], 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "packageManager": "pnpm@10.6.2", 20 | "dependencies": { 21 | "@modelcontextprotocol/sdk": "^1.9.0", 22 | "dotenv": "^16.4.7", 23 | "zod": "^3.24.2" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^22.14.0", 27 | "typescript": "^5.8.3" 28 | } 29 | } -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@modelcontextprotocol/sdk': 12 | specifier: ^1.9.0 13 | version: 1.9.0 14 | dotenv: 15 | specifier: ^16.4.7 16 | version: 16.4.7 17 | zod: 18 | specifier: ^3.24.2 19 | version: 3.24.2 20 | devDependencies: 21 | '@types/node': 22 | specifier: ^22.14.0 23 | version: 22.14.0 24 | typescript: 25 | specifier: ^5.8.3 26 | version: 5.8.3 27 | 28 | packages: 29 | 30 | '@modelcontextprotocol/sdk@1.9.0': 31 | resolution: {integrity: sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==} 32 | engines: {node: '>=18'} 33 | 34 | '@types/node@22.14.0': 35 | resolution: {integrity: sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==} 36 | 37 | accepts@2.0.0: 38 | resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} 39 | engines: {node: '>= 0.6'} 40 | 41 | body-parser@2.2.0: 42 | resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} 43 | engines: {node: '>=18'} 44 | 45 | bytes@3.1.2: 46 | resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} 47 | engines: {node: '>= 0.8'} 48 | 49 | call-bind-apply-helpers@1.0.2: 50 | resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} 51 | engines: {node: '>= 0.4'} 52 | 53 | call-bound@1.0.4: 54 | resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} 55 | engines: {node: '>= 0.4'} 56 | 57 | content-disposition@1.0.0: 58 | resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} 59 | engines: {node: '>= 0.6'} 60 | 61 | content-type@1.0.5: 62 | resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} 63 | engines: {node: '>= 0.6'} 64 | 65 | cookie-signature@1.2.2: 66 | resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} 67 | engines: {node: '>=6.6.0'} 68 | 69 | cookie@0.7.2: 70 | resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} 71 | engines: {node: '>= 0.6'} 72 | 73 | cors@2.8.5: 74 | resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} 75 | engines: {node: '>= 0.10'} 76 | 77 | cross-spawn@7.0.6: 78 | resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 79 | engines: {node: '>= 8'} 80 | 81 | debug@4.4.0: 82 | resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} 83 | engines: {node: '>=6.0'} 84 | peerDependencies: 85 | supports-color: '*' 86 | peerDependenciesMeta: 87 | supports-color: 88 | optional: true 89 | 90 | depd@2.0.0: 91 | resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} 92 | engines: {node: '>= 0.8'} 93 | 94 | dotenv@16.4.7: 95 | resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} 96 | engines: {node: '>=12'} 97 | 98 | dunder-proto@1.0.1: 99 | resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} 100 | engines: {node: '>= 0.4'} 101 | 102 | ee-first@1.1.1: 103 | resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} 104 | 105 | encodeurl@2.0.0: 106 | resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} 107 | engines: {node: '>= 0.8'} 108 | 109 | es-define-property@1.0.1: 110 | resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} 111 | engines: {node: '>= 0.4'} 112 | 113 | es-errors@1.3.0: 114 | resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} 115 | engines: {node: '>= 0.4'} 116 | 117 | es-object-atoms@1.1.1: 118 | resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} 119 | engines: {node: '>= 0.4'} 120 | 121 | escape-html@1.0.3: 122 | resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} 123 | 124 | etag@1.8.1: 125 | resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} 126 | engines: {node: '>= 0.6'} 127 | 128 | eventsource-parser@3.0.1: 129 | resolution: {integrity: sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==} 130 | engines: {node: '>=18.0.0'} 131 | 132 | eventsource@3.0.6: 133 | resolution: {integrity: sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==} 134 | engines: {node: '>=18.0.0'} 135 | 136 | express-rate-limit@7.5.0: 137 | resolution: {integrity: sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==} 138 | engines: {node: '>= 16'} 139 | peerDependencies: 140 | express: ^4.11 || 5 || ^5.0.0-beta.1 141 | 142 | express@5.1.0: 143 | resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} 144 | engines: {node: '>= 18'} 145 | 146 | finalhandler@2.1.0: 147 | resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} 148 | engines: {node: '>= 0.8'} 149 | 150 | forwarded@0.2.0: 151 | resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} 152 | engines: {node: '>= 0.6'} 153 | 154 | fresh@2.0.0: 155 | resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} 156 | engines: {node: '>= 0.8'} 157 | 158 | function-bind@1.1.2: 159 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 160 | 161 | get-intrinsic@1.3.0: 162 | resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} 163 | engines: {node: '>= 0.4'} 164 | 165 | get-proto@1.0.1: 166 | resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} 167 | engines: {node: '>= 0.4'} 168 | 169 | gopd@1.2.0: 170 | resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} 171 | engines: {node: '>= 0.4'} 172 | 173 | has-symbols@1.1.0: 174 | resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} 175 | engines: {node: '>= 0.4'} 176 | 177 | hasown@2.0.2: 178 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 179 | engines: {node: '>= 0.4'} 180 | 181 | http-errors@2.0.0: 182 | resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} 183 | engines: {node: '>= 0.8'} 184 | 185 | iconv-lite@0.6.3: 186 | resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} 187 | engines: {node: '>=0.10.0'} 188 | 189 | inherits@2.0.4: 190 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 191 | 192 | ipaddr.js@1.9.1: 193 | resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} 194 | engines: {node: '>= 0.10'} 195 | 196 | is-promise@4.0.0: 197 | resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} 198 | 199 | isexe@2.0.0: 200 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 201 | 202 | math-intrinsics@1.1.0: 203 | resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} 204 | engines: {node: '>= 0.4'} 205 | 206 | media-typer@1.1.0: 207 | resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} 208 | engines: {node: '>= 0.8'} 209 | 210 | merge-descriptors@2.0.0: 211 | resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} 212 | engines: {node: '>=18'} 213 | 214 | mime-db@1.54.0: 215 | resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} 216 | engines: {node: '>= 0.6'} 217 | 218 | mime-types@3.0.1: 219 | resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} 220 | engines: {node: '>= 0.6'} 221 | 222 | ms@2.1.3: 223 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 224 | 225 | negotiator@1.0.0: 226 | resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} 227 | engines: {node: '>= 0.6'} 228 | 229 | object-assign@4.1.1: 230 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 231 | engines: {node: '>=0.10.0'} 232 | 233 | object-inspect@1.13.4: 234 | resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} 235 | engines: {node: '>= 0.4'} 236 | 237 | on-finished@2.4.1: 238 | resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} 239 | engines: {node: '>= 0.8'} 240 | 241 | once@1.4.0: 242 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 243 | 244 | parseurl@1.3.3: 245 | resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} 246 | engines: {node: '>= 0.8'} 247 | 248 | path-key@3.1.1: 249 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 250 | engines: {node: '>=8'} 251 | 252 | path-to-regexp@8.2.0: 253 | resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} 254 | engines: {node: '>=16'} 255 | 256 | pkce-challenge@5.0.0: 257 | resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==} 258 | engines: {node: '>=16.20.0'} 259 | 260 | proxy-addr@2.0.7: 261 | resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} 262 | engines: {node: '>= 0.10'} 263 | 264 | qs@6.14.0: 265 | resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} 266 | engines: {node: '>=0.6'} 267 | 268 | range-parser@1.2.1: 269 | resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} 270 | engines: {node: '>= 0.6'} 271 | 272 | raw-body@3.0.0: 273 | resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} 274 | engines: {node: '>= 0.8'} 275 | 276 | router@2.2.0: 277 | resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} 278 | engines: {node: '>= 18'} 279 | 280 | safe-buffer@5.2.1: 281 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 282 | 283 | safer-buffer@2.1.2: 284 | resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 285 | 286 | send@1.2.0: 287 | resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} 288 | engines: {node: '>= 18'} 289 | 290 | serve-static@2.2.0: 291 | resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} 292 | engines: {node: '>= 18'} 293 | 294 | setprototypeof@1.2.0: 295 | resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} 296 | 297 | shebang-command@2.0.0: 298 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 299 | engines: {node: '>=8'} 300 | 301 | shebang-regex@3.0.0: 302 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 303 | engines: {node: '>=8'} 304 | 305 | side-channel-list@1.0.0: 306 | resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} 307 | engines: {node: '>= 0.4'} 308 | 309 | side-channel-map@1.0.1: 310 | resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} 311 | engines: {node: '>= 0.4'} 312 | 313 | side-channel-weakmap@1.0.2: 314 | resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} 315 | engines: {node: '>= 0.4'} 316 | 317 | side-channel@1.1.0: 318 | resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} 319 | engines: {node: '>= 0.4'} 320 | 321 | statuses@2.0.1: 322 | resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} 323 | engines: {node: '>= 0.8'} 324 | 325 | toidentifier@1.0.1: 326 | resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} 327 | engines: {node: '>=0.6'} 328 | 329 | type-is@2.0.1: 330 | resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} 331 | engines: {node: '>= 0.6'} 332 | 333 | typescript@5.8.3: 334 | resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} 335 | engines: {node: '>=14.17'} 336 | hasBin: true 337 | 338 | undici-types@6.21.0: 339 | resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} 340 | 341 | unpipe@1.0.0: 342 | resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} 343 | engines: {node: '>= 0.8'} 344 | 345 | vary@1.1.2: 346 | resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} 347 | engines: {node: '>= 0.8'} 348 | 349 | which@2.0.2: 350 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 351 | engines: {node: '>= 8'} 352 | hasBin: true 353 | 354 | wrappy@1.0.2: 355 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 356 | 357 | zod-to-json-schema@3.24.5: 358 | resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} 359 | peerDependencies: 360 | zod: ^3.24.1 361 | 362 | zod@3.24.2: 363 | resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} 364 | 365 | snapshots: 366 | 367 | '@modelcontextprotocol/sdk@1.9.0': 368 | dependencies: 369 | content-type: 1.0.5 370 | cors: 2.8.5 371 | cross-spawn: 7.0.6 372 | eventsource: 3.0.6 373 | express: 5.1.0 374 | express-rate-limit: 7.5.0(express@5.1.0) 375 | pkce-challenge: 5.0.0 376 | raw-body: 3.0.0 377 | zod: 3.24.2 378 | zod-to-json-schema: 3.24.5(zod@3.24.2) 379 | transitivePeerDependencies: 380 | - supports-color 381 | 382 | '@types/node@22.14.0': 383 | dependencies: 384 | undici-types: 6.21.0 385 | 386 | accepts@2.0.0: 387 | dependencies: 388 | mime-types: 3.0.1 389 | negotiator: 1.0.0 390 | 391 | body-parser@2.2.0: 392 | dependencies: 393 | bytes: 3.1.2 394 | content-type: 1.0.5 395 | debug: 4.4.0 396 | http-errors: 2.0.0 397 | iconv-lite: 0.6.3 398 | on-finished: 2.4.1 399 | qs: 6.14.0 400 | raw-body: 3.0.0 401 | type-is: 2.0.1 402 | transitivePeerDependencies: 403 | - supports-color 404 | 405 | bytes@3.1.2: {} 406 | 407 | call-bind-apply-helpers@1.0.2: 408 | dependencies: 409 | es-errors: 1.3.0 410 | function-bind: 1.1.2 411 | 412 | call-bound@1.0.4: 413 | dependencies: 414 | call-bind-apply-helpers: 1.0.2 415 | get-intrinsic: 1.3.0 416 | 417 | content-disposition@1.0.0: 418 | dependencies: 419 | safe-buffer: 5.2.1 420 | 421 | content-type@1.0.5: {} 422 | 423 | cookie-signature@1.2.2: {} 424 | 425 | cookie@0.7.2: {} 426 | 427 | cors@2.8.5: 428 | dependencies: 429 | object-assign: 4.1.1 430 | vary: 1.1.2 431 | 432 | cross-spawn@7.0.6: 433 | dependencies: 434 | path-key: 3.1.1 435 | shebang-command: 2.0.0 436 | which: 2.0.2 437 | 438 | debug@4.4.0: 439 | dependencies: 440 | ms: 2.1.3 441 | 442 | depd@2.0.0: {} 443 | 444 | dotenv@16.4.7: {} 445 | 446 | dunder-proto@1.0.1: 447 | dependencies: 448 | call-bind-apply-helpers: 1.0.2 449 | es-errors: 1.3.0 450 | gopd: 1.2.0 451 | 452 | ee-first@1.1.1: {} 453 | 454 | encodeurl@2.0.0: {} 455 | 456 | es-define-property@1.0.1: {} 457 | 458 | es-errors@1.3.0: {} 459 | 460 | es-object-atoms@1.1.1: 461 | dependencies: 462 | es-errors: 1.3.0 463 | 464 | escape-html@1.0.3: {} 465 | 466 | etag@1.8.1: {} 467 | 468 | eventsource-parser@3.0.1: {} 469 | 470 | eventsource@3.0.6: 471 | dependencies: 472 | eventsource-parser: 3.0.1 473 | 474 | express-rate-limit@7.5.0(express@5.1.0): 475 | dependencies: 476 | express: 5.1.0 477 | 478 | express@5.1.0: 479 | dependencies: 480 | accepts: 2.0.0 481 | body-parser: 2.2.0 482 | content-disposition: 1.0.0 483 | content-type: 1.0.5 484 | cookie: 0.7.2 485 | cookie-signature: 1.2.2 486 | debug: 4.4.0 487 | encodeurl: 2.0.0 488 | escape-html: 1.0.3 489 | etag: 1.8.1 490 | finalhandler: 2.1.0 491 | fresh: 2.0.0 492 | http-errors: 2.0.0 493 | merge-descriptors: 2.0.0 494 | mime-types: 3.0.1 495 | on-finished: 2.4.1 496 | once: 1.4.0 497 | parseurl: 1.3.3 498 | proxy-addr: 2.0.7 499 | qs: 6.14.0 500 | range-parser: 1.2.1 501 | router: 2.2.0 502 | send: 1.2.0 503 | serve-static: 2.2.0 504 | statuses: 2.0.1 505 | type-is: 2.0.1 506 | vary: 1.1.2 507 | transitivePeerDependencies: 508 | - supports-color 509 | 510 | finalhandler@2.1.0: 511 | dependencies: 512 | debug: 4.4.0 513 | encodeurl: 2.0.0 514 | escape-html: 1.0.3 515 | on-finished: 2.4.1 516 | parseurl: 1.3.3 517 | statuses: 2.0.1 518 | transitivePeerDependencies: 519 | - supports-color 520 | 521 | forwarded@0.2.0: {} 522 | 523 | fresh@2.0.0: {} 524 | 525 | function-bind@1.1.2: {} 526 | 527 | get-intrinsic@1.3.0: 528 | dependencies: 529 | call-bind-apply-helpers: 1.0.2 530 | es-define-property: 1.0.1 531 | es-errors: 1.3.0 532 | es-object-atoms: 1.1.1 533 | function-bind: 1.1.2 534 | get-proto: 1.0.1 535 | gopd: 1.2.0 536 | has-symbols: 1.1.0 537 | hasown: 2.0.2 538 | math-intrinsics: 1.1.0 539 | 540 | get-proto@1.0.1: 541 | dependencies: 542 | dunder-proto: 1.0.1 543 | es-object-atoms: 1.1.1 544 | 545 | gopd@1.2.0: {} 546 | 547 | has-symbols@1.1.0: {} 548 | 549 | hasown@2.0.2: 550 | dependencies: 551 | function-bind: 1.1.2 552 | 553 | http-errors@2.0.0: 554 | dependencies: 555 | depd: 2.0.0 556 | inherits: 2.0.4 557 | setprototypeof: 1.2.0 558 | statuses: 2.0.1 559 | toidentifier: 1.0.1 560 | 561 | iconv-lite@0.6.3: 562 | dependencies: 563 | safer-buffer: 2.1.2 564 | 565 | inherits@2.0.4: {} 566 | 567 | ipaddr.js@1.9.1: {} 568 | 569 | is-promise@4.0.0: {} 570 | 571 | isexe@2.0.0: {} 572 | 573 | math-intrinsics@1.1.0: {} 574 | 575 | media-typer@1.1.0: {} 576 | 577 | merge-descriptors@2.0.0: {} 578 | 579 | mime-db@1.54.0: {} 580 | 581 | mime-types@3.0.1: 582 | dependencies: 583 | mime-db: 1.54.0 584 | 585 | ms@2.1.3: {} 586 | 587 | negotiator@1.0.0: {} 588 | 589 | object-assign@4.1.1: {} 590 | 591 | object-inspect@1.13.4: {} 592 | 593 | on-finished@2.4.1: 594 | dependencies: 595 | ee-first: 1.1.1 596 | 597 | once@1.4.0: 598 | dependencies: 599 | wrappy: 1.0.2 600 | 601 | parseurl@1.3.3: {} 602 | 603 | path-key@3.1.1: {} 604 | 605 | path-to-regexp@8.2.0: {} 606 | 607 | pkce-challenge@5.0.0: {} 608 | 609 | proxy-addr@2.0.7: 610 | dependencies: 611 | forwarded: 0.2.0 612 | ipaddr.js: 1.9.1 613 | 614 | qs@6.14.0: 615 | dependencies: 616 | side-channel: 1.1.0 617 | 618 | range-parser@1.2.1: {} 619 | 620 | raw-body@3.0.0: 621 | dependencies: 622 | bytes: 3.1.2 623 | http-errors: 2.0.0 624 | iconv-lite: 0.6.3 625 | unpipe: 1.0.0 626 | 627 | router@2.2.0: 628 | dependencies: 629 | debug: 4.4.0 630 | depd: 2.0.0 631 | is-promise: 4.0.0 632 | parseurl: 1.3.3 633 | path-to-regexp: 8.2.0 634 | transitivePeerDependencies: 635 | - supports-color 636 | 637 | safe-buffer@5.2.1: {} 638 | 639 | safer-buffer@2.1.2: {} 640 | 641 | send@1.2.0: 642 | dependencies: 643 | debug: 4.4.0 644 | encodeurl: 2.0.0 645 | escape-html: 1.0.3 646 | etag: 1.8.1 647 | fresh: 2.0.0 648 | http-errors: 2.0.0 649 | mime-types: 3.0.1 650 | ms: 2.1.3 651 | on-finished: 2.4.1 652 | range-parser: 1.2.1 653 | statuses: 2.0.1 654 | transitivePeerDependencies: 655 | - supports-color 656 | 657 | serve-static@2.2.0: 658 | dependencies: 659 | encodeurl: 2.0.0 660 | escape-html: 1.0.3 661 | parseurl: 1.3.3 662 | send: 1.2.0 663 | transitivePeerDependencies: 664 | - supports-color 665 | 666 | setprototypeof@1.2.0: {} 667 | 668 | shebang-command@2.0.0: 669 | dependencies: 670 | shebang-regex: 3.0.0 671 | 672 | shebang-regex@3.0.0: {} 673 | 674 | side-channel-list@1.0.0: 675 | dependencies: 676 | es-errors: 1.3.0 677 | object-inspect: 1.13.4 678 | 679 | side-channel-map@1.0.1: 680 | dependencies: 681 | call-bound: 1.0.4 682 | es-errors: 1.3.0 683 | get-intrinsic: 1.3.0 684 | object-inspect: 1.13.4 685 | 686 | side-channel-weakmap@1.0.2: 687 | dependencies: 688 | call-bound: 1.0.4 689 | es-errors: 1.3.0 690 | get-intrinsic: 1.3.0 691 | object-inspect: 1.13.4 692 | side-channel-map: 1.0.1 693 | 694 | side-channel@1.1.0: 695 | dependencies: 696 | es-errors: 1.3.0 697 | object-inspect: 1.13.4 698 | side-channel-list: 1.0.0 699 | side-channel-map: 1.0.1 700 | side-channel-weakmap: 1.0.2 701 | 702 | statuses@2.0.1: {} 703 | 704 | toidentifier@1.0.1: {} 705 | 706 | type-is@2.0.1: 707 | dependencies: 708 | content-type: 1.0.5 709 | media-typer: 1.1.0 710 | mime-types: 3.0.1 711 | 712 | typescript@5.8.3: {} 713 | 714 | undici-types@6.21.0: {} 715 | 716 | unpipe@1.0.0: {} 717 | 718 | vary@1.1.2: {} 719 | 720 | which@2.0.2: 721 | dependencies: 722 | isexe: 2.0.0 723 | 724 | wrappy@1.0.2: {} 725 | 726 | zod-to-json-schema@3.24.5(zod@3.24.2): 727 | dependencies: 728 | zod: 3.24.2 729 | 730 | zod@3.24.2: {} 731 | -------------------------------------------------------------------------------- /src/api/blogs.ts: -------------------------------------------------------------------------------- 1 | import { BlogResponse, ReducedItemData } from "../types/api/blogs.js"; 2 | import { InterviewReadyUrls } from "../config/urls.js"; 3 | 4 | export class BlogPosts { 5 | private static cachedResponse: ReducedItemData[] | null = null; 6 | 7 | static async getAllBlogPosts() { 8 | if (this.cachedResponse) { 9 | return this.cachedResponse; 10 | } 11 | 12 | const response = await fetch(InterviewReadyUrls.BLOG_API_URL); 13 | if (!response.ok) { 14 | throw new Error("Failed to fetch blog posts"); 15 | } 16 | 17 | const data: BlogResponse = await response.json(); 18 | const blogData = data._collections[0]._data; 19 | const reducedBlogData: ReducedItemData[] = blogData.map((post) => ({ 20 | slug: post.slug, 21 | title: post.title, 22 | keywords: post.keywords || post.Keywords, 23 | bodyPlainText: post.bodyPlainText, 24 | url: `${InterviewReadyUrls.BASE_URL}${post.path}` 25 | })); 26 | 27 | this.cachedResponse = reducedBlogData; 28 | return reducedBlogData; 29 | } 30 | 31 | static async getBlogPostBySlug(slug: string) { 32 | const blogPosts = await this.getAllBlogPosts(); 33 | return blogPosts.find((post) => post.slug === slug); 34 | } 35 | 36 | static async getBlogPostByKeywords(keyword: string) { 37 | const blogPosts = await this.getAllBlogPosts(); 38 | return blogPosts.filter((post) => post.keywords?.includes(keyword)); 39 | } 40 | 41 | static async getBlogPostByTitle(title: string) { 42 | const blogPosts = await this.getAllBlogPosts(); 43 | return blogPosts.find((post) => post.title === title); 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/api/courses.ts: -------------------------------------------------------------------------------- 1 | import { InterviewReadyUrls } from "../config/urls.js" 2 | import { CourseResponse, CoursesWithStatsResponse } from "../types/api/courses.js" 3 | 4 | export async function getCourses(): Promise { 5 | const API = InterviewReadyUrls.COURSES_API_URL 6 | const response = await fetch(API) 7 | if (!response.ok) { 8 | throw new Error("Failed to fetch courses") 9 | } 10 | 11 | const data: CoursesWithStatsResponse = await response.json() 12 | const courses = data.courses 13 | return courses 14 | } 15 | 16 | export async function getCourseById(courseId: number): Promise { 17 | const response = await fetch(`${InterviewReadyUrls.COURSES_API_URL}/${courseId}`); 18 | if (!response.ok) { 19 | throw new Error(`Failed to fetch course with id: ${courseId}`); 20 | } 21 | const data = await response.json(); 22 | return data; 23 | } -------------------------------------------------------------------------------- /src/api/resource.ts: -------------------------------------------------------------------------------- 1 | import { InterviewReadyUrls } from "../config/urls.js"; 2 | import { Resource } from "../types/api/resource.js"; 3 | 4 | export async function getResources(): Promise { 5 | const response = await fetch(InterviewReadyUrls.RESOURCE_API_URL); 6 | if (!response.ok) { 7 | throw new Error("Failed to fetch resources"); 8 | } 9 | 10 | const data = await response.json(); 11 | return data; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/config/env.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import path from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = path.dirname(__filename); 7 | dotenv.config({ path: path.resolve(__dirname, '../../.env') }); 8 | 9 | export const env = { 10 | BASE_URL: process.env.BASE_URL, 11 | API_URL: process.env.API_URL, 12 | } as const; 13 | 14 | const requiredEnvVars: (keyof typeof env)[] = ['BASE_URL', 'API_URL']; 15 | 16 | for (const envVar of requiredEnvVars) { 17 | if (!env[envVar]) { 18 | throw new Error(`Missing required environment variable: ${envVar}`); 19 | } 20 | } -------------------------------------------------------------------------------- /src/config/urls.ts: -------------------------------------------------------------------------------- 1 | import { env } from './env.js'; 2 | 3 | export const InterviewReadyUrls = { 4 | BASE_URL: env.BASE_URL, 5 | API_URL: `${env.API_URL}/api`, 6 | BLOG_API_URL: `${env.BASE_URL}/_nuxt/content/db-96806b65.json`, 7 | COURSES_API_URL: `${env.API_URL}/v1/courses`, 8 | RESOURCE_API_URL: `${env.API_URL}/utilities/resources/all` 9 | }; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { CoursesTool } from "./tools/CourseTool.js"; 4 | import { BlogTool } from "./tools/BlogTool.js"; 5 | import { ResourceTool } from "./tools/ResourceTool.js"; 6 | 7 | const server = new McpServer({ 8 | name: "interviewready", 9 | version: "1.0.0", 10 | capabilities: { 11 | resources: {}, 12 | tools: {}, 13 | }, 14 | }); 15 | 16 | new CoursesTool(server) 17 | new BlogTool(server) 18 | new ResourceTool(server) 19 | 20 | async function main() { 21 | const transport = new StdioServerTransport(); 22 | await server.connect(transport); 23 | console.error("InterviewReady MCP Server running on stdio"); 24 | } 25 | 26 | main().catch((error) => { 27 | console.error("Fatal error in main():", error); 28 | process.exit(1); 29 | }); -------------------------------------------------------------------------------- /src/middleware/withError.ts: -------------------------------------------------------------------------------- 1 | export const withError = (fn: (...args: any[]) => Promise) => { 2 | return async (...args: any[]) => { 3 | try { 4 | return await fn(...args); 5 | } catch (error) { 6 | console.error(error); 7 | throw { 8 | content: [ 9 | { type: "text" as const, text: JSON.stringify(error) } 10 | ] 11 | }; 12 | } 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/tools/BlogTool.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { z } from "zod"; 3 | import { BlogPosts } from "../api/blogs.js"; 4 | import { ReducedItemData } from "../types/api/blogs.js"; 5 | import { withError } from "../middleware/withError.js"; 6 | 7 | export class BlogTool { 8 | constructor(private readonly server: McpServer) { 9 | this.server = server; 10 | this.registerTool(); 11 | } 12 | 13 | private registerTool() { 14 | this.server.tool( 15 | "get-blog-posts", 16 | "Get all blog posts on interviewready platform", 17 | {}, 18 | withError(async () => this.getBlogPosts()), 19 | ); 20 | 21 | this.server.tool( 22 | "get-blog-post-by-slug", 23 | "Get a blog post by slug", 24 | { 25 | slug: z.string().describe("The slug of the blog post to get"), 26 | }, 27 | withError(async ({ slug }) => this.getBlogPostBySlug(slug)), 28 | ); 29 | 30 | this.server.tool( 31 | "get-blog-post-by-keywords", 32 | "Get a blog post by keywords", 33 | { 34 | keywords: z.array(z.string()).describe("The keywords of the blog post to get"), 35 | }, 36 | withError(async ({ keywords }) => this.getBlogPostByKeywords(keywords)), 37 | ); 38 | 39 | this.server.tool( 40 | "get-blog-post-by-title", 41 | "Get a blog post by title", 42 | { 43 | title: z.string().describe("The title of the blog post to get"), 44 | }, 45 | withError(async ({ title }) => this.getBlogPostByTitle(title)), 46 | ); 47 | 48 | this.server.tool( 49 | "blog-content", 50 | "Get content of a blog post by title", 51 | { 52 | title: z.string().describe("The title of the blog post to get"), 53 | }, 54 | withError(async ({ title }) => this.getBlogContent(title)), 55 | ); 56 | } 57 | 58 | private getBlogPostWithoutBody(blogPost: ReducedItemData) { 59 | return { 60 | slug: blogPost.slug, 61 | title: blogPost.title, 62 | keywords: blogPost.keywords, 63 | url: blogPost.url 64 | }; 65 | } 66 | 67 | async getBlogPosts() { 68 | const blogPosts = await BlogPosts.getAllBlogPosts(); 69 | const content = blogPosts.map((blogPost) => ( 70 | { 71 | type: "text" as const, 72 | text: JSON.stringify(this.getBlogPostWithoutBody(blogPost)) 73 | } 74 | )) 75 | 76 | return { content: content }; 77 | } 78 | 79 | async getBlogPostBySlug(slug: string) { 80 | const blogPost = await BlogPosts.getBlogPostBySlug(slug); 81 | if (!blogPost) { 82 | return { 83 | content: [ 84 | { type: "text" as const, text: "No blog post found" } 85 | ] 86 | }; 87 | } 88 | 89 | return { 90 | content: [ 91 | { 92 | type: "text" as const, 93 | text: JSON.stringify(blogPost), 94 | } 95 | ] 96 | }; 97 | } 98 | 99 | async getBlogPostByKeywords(keywords: string[]) { 100 | const keywordPromises = keywords.map(keyword => BlogPosts.getBlogPostByKeywords(keyword)); 101 | const blogPostsArrays = await Promise.all(keywordPromises); 102 | const allBlogPosts = blogPostsArrays.flat(); 103 | 104 | if (allBlogPosts.length == 0) { 105 | return { 106 | content: [ 107 | { type: "text" as const, text: "No blog post found" } 108 | ] 109 | }; 110 | } 111 | 112 | const content = allBlogPosts.map(blogPost => ( 113 | { type: "text" as const, text: JSON.stringify(this.getBlogPostWithoutBody(blogPost)) })) 114 | return { content: content }; 115 | } 116 | 117 | async getBlogPostByTitle(title: string) { 118 | const blogPost = await BlogPosts.getBlogPostByTitle(title); 119 | if (!blogPost) { 120 | return { 121 | content: [ 122 | { type: "text" as const, text: "No blog post found" } 123 | ] 124 | }; 125 | } 126 | 127 | return { 128 | content: [ 129 | { type: "text" as const, text: JSON.stringify(blogPost) } 130 | ] 131 | }; 132 | } 133 | 134 | async getBlogContent(title: string) { 135 | const blogPost = await BlogPosts.getBlogPostByTitle(title); 136 | if (!blogPost) { 137 | return { 138 | content: [ 139 | { type: "text" as const, text: "No blog post found" } 140 | ] 141 | }; 142 | } 143 | 144 | return { 145 | content: [ 146 | { type: "text" as const, text: blogPost.bodyPlainText } 147 | ] 148 | }; 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /src/tools/CourseTool.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { getCourseById, getCourses } from "../api/courses.js"; 3 | import { z } from "zod"; 4 | import { withError } from "../middleware/withError.js"; 5 | export class CoursesTool { 6 | constructor(private readonly server: McpServer) { 7 | this.server = server; 8 | this.registerTool(); 9 | } 10 | 11 | private registerTool() { 12 | this.server.tool( 13 | "get-courses", 14 | "Get all courses on interviewready platform", 15 | {}, 16 | withError(async () => this.getCourses()), 17 | ); 18 | 19 | this.server.tool( 20 | "get-course-by-id", 21 | "Get a course by id", 22 | { 23 | courseId: z.number().describe("The id of the course to get"), 24 | }, 25 | withError(async ({ courseId }) => this.getCourseById(courseId)), 26 | ); 27 | } 28 | 29 | async getCourses() { 30 | const courses = await getCourses(); 31 | const relevantData = courses.map((course) => ({ 32 | id: course.id, 33 | name: course.name, 34 | description: course.description, 35 | thumbnail_url: course.thumbnail_url, 36 | })); 37 | 38 | return { 39 | content: [ 40 | { 41 | type: "text" as const, 42 | text: JSON.stringify(relevantData), 43 | }, 44 | ], 45 | }; 46 | } 47 | 48 | async getCourseById(courseId: number) { 49 | const course = await getCourseById(courseId); 50 | 51 | return { 52 | content: [ 53 | { 54 | type: "text" as const, 55 | text: JSON.stringify(course), 56 | }, 57 | ], 58 | }; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/tools/ResourceTool.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { getResources } from "../api/resource.js"; 3 | import { withError } from "../middleware/withError.js"; 4 | export class ResourceTool { 5 | constructor(private readonly server: McpServer) { 6 | this.server = server; 7 | this.registerTool(); 8 | } 9 | 10 | private registerTool() { 11 | this.server.tool( 12 | "get-resources", 13 | "Get all external resources recommended by interviewready platform", 14 | {}, 15 | withError(async () => this.getResources()), 16 | ); 17 | } 18 | 19 | private async getResources() { 20 | const resources = await getResources(); 21 | const content = resources.map(resource => ({ type: "text" as const, text: JSON.stringify(resource) })); 22 | return { content: content }; 23 | } 24 | } -------------------------------------------------------------------------------- /src/types/api/blogs.ts: -------------------------------------------------------------------------------- 1 | interface TextNode { 2 | type: "text"; 3 | value: string; 4 | } 5 | 6 | interface ElementNode { 7 | type: "element"; 8 | tag: string; 9 | props: Record; 10 | children: Array; 11 | } 12 | 13 | type BodyChild = TextNode | ElementNode; 14 | 15 | interface Body { 16 | type: "root"; 17 | children: BodyChild[]; 18 | } 19 | 20 | interface TocEntry { 21 | id: string; 22 | depth: number; 23 | text: string; 24 | } 25 | 26 | export interface ItemData { 27 | slug: string; 28 | title: string; 29 | keywords?: string; 30 | Keywords?: string; 31 | text: string; 32 | bodyPlainText: string; 33 | createdAt: Date; 34 | updatedAt: Date; 35 | toc: TocEntry[]; 36 | body: Body; 37 | meta?: Record; 38 | path: string; 39 | } 40 | 41 | export interface ReducedItemData { 42 | slug: string; 43 | title: string; 44 | keywords?: string; 45 | bodyPlainText: string; 46 | url: string; 47 | } 48 | 49 | interface Collection { 50 | name: string; 51 | unindexedSortComparator: string; 52 | defaultLokiOperatorPackage: string; 53 | _dynamicViews: any[]; 54 | uniqueNames: string[]; 55 | transforms: Record; 56 | rangedIndexes: Record; 57 | _data: ItemData[]; 58 | } 59 | 60 | export interface BlogResponse { 61 | _env: string; 62 | _serializationMethod: string; 63 | _autosave: boolean; 64 | _autosaveInterval: number; 65 | _collections: Collection[]; 66 | } -------------------------------------------------------------------------------- /src/types/api/courses.ts: -------------------------------------------------------------------------------- 1 | export interface PlatformStats { 2 | total_enrolled_users: number; 3 | total_videos_watched: number; 4 | total_sdoj_submission: number; 5 | } 6 | 7 | export interface Instructor { 8 | id: number; 9 | course_id: number; 10 | name: string; 11 | description: string; 12 | image_url: string; 13 | experience: string; 14 | social_links: string[]; 15 | created: number; 16 | updated: number; 17 | } 18 | 19 | export interface CoursePlan { 20 | id: number; 21 | course_id: number; 22 | course_plan_name: string; 23 | validity: number; 24 | zoom_sessions: boolean; 25 | price_inr: number; 26 | price_usd: number; 27 | active: boolean; 28 | created: number; 29 | updated: number; 30 | } 31 | 32 | export interface Course { 33 | id: number; 34 | name: string; 35 | slug: string; 36 | description: string; 37 | keywords: string; 38 | updated: number; 39 | created: number; 40 | video_id: number; 41 | course_rating: number; 42 | thumbnail_url: string; 43 | 44 | num_of_videos: number; 45 | num_of_quizzes: number; 46 | num_of_downloadable_resources: number; 47 | num_of_ratings: number; 48 | num_of_enrolled_users: number; 49 | num_of_discussions: number; 50 | 51 | instructors: Instructor[]; 52 | course_plans: CoursePlan[]; 53 | 54 | playback_url: string; 55 | tags: string[]; 56 | last_updated: number; 57 | } 58 | 59 | export interface CoursesWithStatsResponse { 60 | platform_stats: PlatformStats; 61 | courses: Course[]; 62 | } 63 | 64 | export type CourseResponse = Course; 65 | 66 | export interface CreateCourseRequest extends Omit { 67 | } 68 | 69 | export interface UpdateCourseRequest extends Partial { 70 | } 71 | -------------------------------------------------------------------------------- /src/types/api/resource.ts: -------------------------------------------------------------------------------- 1 | export interface Resource { 2 | id: number; 3 | title: string; 4 | company: string; 5 | topic: string; 6 | type: string; 7 | difficulty: string; 8 | link: string; 9 | created: string; 10 | updated: string; 11 | } 12 | -------------------------------------------------------------------------------- /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": [ 14 | "src/**/*" 15 | ], 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } --------------------------------------------------------------------------------