whistler-mcp 1.0.8 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,7 @@ export function getSavedToken() {
11
11
  try {
12
12
  const data = JSON.parse(fs.readFileSync(TOKEN_PATH, 'utf8'));
13
13
  return data.token;
14
- } catch(e) {}
14
+ } catch (e) { }
15
15
  }
16
16
  return process.env.WHISTLER_API_TOKEN || null;
17
17
  }
@@ -29,7 +29,7 @@ export function clearToken() {
29
29
  */
30
30
  export async function invokeApi(endpoint, method = 'GET', body = null) {
31
31
  const headers = { 'Content-Type': 'application/json' };
32
-
32
+
33
33
  const token = getSavedToken();
34
34
  if (!token) {
35
35
  throw new Error('Not authenticated. Please ask the user for their email and password, and run the login_to_whistler tool to log them in automatically.');
@@ -49,7 +49,7 @@ export async function invokeApi(endpoint, method = 'GET', body = null) {
49
49
  } catch (error) {
50
50
  throw new Error(`Failed to connect to API at ${API_BASE_URL}. Is the server running? ${error.message}`);
51
51
  }
52
-
52
+
53
53
  let data;
54
54
  try {
55
55
  data = await response.json();
@@ -65,7 +65,7 @@ export async function invokeApi(endpoint, method = 'GET', body = null) {
65
65
  }
66
66
  throw new Error(`API Error ${response.status}: ${data.message || JSON.stringify(data)}`);
67
67
  }
68
-
68
+
69
69
  return data;
70
70
  }
71
71
 
@@ -74,7 +74,7 @@ export async function invokeApi(endpoint, method = 'GET', body = null) {
74
74
  */
75
75
  export async function invokeApiBinary(endpoint, method = 'GET', body = null) {
76
76
  const headers = {};
77
-
77
+
78
78
  const token = getSavedToken();
79
79
  if (!token) {
80
80
  throw new Error('Not authenticated. Please ask the user for their email and password, and run the login_to_whistler tool to log them in automatically.');
@@ -103,6 +103,6 @@ export async function invokeApiBinary(endpoint, method = 'GET', body = null) {
103
103
  const text = await response.text();
104
104
  throw new Error(`API Error ${response.status}: ${text}`);
105
105
  }
106
-
106
+
107
107
  return await response.arrayBuffer();
108
108
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whistler-mcp",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "An MCP Server for retrieving placement data, job roles, and platform statistics from the Whistler API.",
5
5
  "main": "server.js",
6
6
  "bin": {
package/server.js CHANGED
@@ -44,9 +44,9 @@ const toolsDefinition = {
44
44
  graduationYear: z.number().int(),
45
45
  currentCPI: z.number().min(0).max(10)
46
46
  }),
47
- inputSchema: {
48
- type: "object",
49
- properties: {
47
+ inputSchema: {
48
+ type: "object",
49
+ properties: {
50
50
  name: { type: "string" },
51
51
  email: { type: "string" },
52
52
  password: { type: "string" },
@@ -54,8 +54,8 @@ const toolsDefinition = {
54
54
  program: { type: "string" },
55
55
  graduationYear: { type: "number" },
56
56
  currentCPI: { type: "number" }
57
- },
58
- required: ["name", "email", "password", "branch", "program", "graduationYear", "currentCPI"]
57
+ },
58
+ required: ["name", "email", "password", "branch", "program", "graduationYear", "currentCPI"]
59
59
  }
60
60
  },
61
61
  logout_whistler: {
@@ -75,7 +75,7 @@ const toolsDefinition = {
75
75
  schema: z.object({ companyName: z.string(), year: z.number().optional() }),
76
76
  inputSchema: { type: "object", properties: { companyName: { type: "string" }, year: { type: "number" } }, required: ["companyName"] }
77
77
  },
78
-
78
+
79
79
  // Platform Stats
80
80
  get_public_stats: {
81
81
  description: "Retrieve public platform statistics.",
@@ -129,7 +129,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
129
129
  // Execute logic (Mapping Tools to the API Adapter)
130
130
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
131
131
  const { name, arguments: args } = request.params;
132
-
132
+
133
133
  if (!toolsDefinition[name]) {
134
134
  throw new Error(`Unknown tool: ${name}`);
135
135
  }
@@ -155,19 +155,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
155
155
  headers: { 'Content-Type': 'application/json' },
156
156
  body: JSON.stringify({ email: params.email, password: params.password })
157
157
  });
158
-
158
+
159
159
  // Handle non-JSON or weird responses gracefully
160
160
  let loginData = {};
161
161
  try {
162
162
  loginData = await loginRes.json();
163
- } catch(e) {
163
+ } catch (e) {
164
164
  const text = await loginRes.text();
165
165
  throw new Error(`Login failed with weird response: ${text.substring(0, 100)}`);
166
166
  }
167
167
 
168
168
  if (!loginRes.ok) throw new Error(loginData.message || 'Login failed');
169
169
  if (!loginData.token) throw new Error('Login succeeded but no token was returned.');
170
-
170
+
171
171
  saveToken(loginData.token, loginData);
172
172
  result = { success: true, message: `Authenticated!` };
173
173
  break;
@@ -182,7 +182,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
182
182
  let signupData = {};
183
183
  try {
184
184
  signupData = await signupRes.json();
185
- } catch(e) {
185
+ } catch (e) {
186
186
  const text = await signupRes.text();
187
187
  throw new Error(`Signup failed with weird response: ${text.substring(0, 100)}`);
188
188
  }
@@ -199,10 +199,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
199
199
  result = { success: true, message: 'Logged out.' };
200
200
  break;
201
201
  case 'get_companies':
202
- result = await invokeApi(`/pdfs/companies${params.year ? '?year='+params.year : ''}`);
202
+ result = await invokeApi(`/pdfs/companies${params.year ? '?year=' + params.year : ''}`);
203
203
  break;
204
204
  case 'get_company_roles':
205
- result = await invokeApi(`/pdfs/company/${params.companyName}${params.year ? '?year='+params.year : ''}`, 'GET');
205
+ result = await invokeApi(`/pdfs/company/${params.companyName}${params.year ? '?year=' + params.year : ''}`, 'GET');
206
206
  break;
207
207
  case 'get_public_stats':
208
208
  result = await invokeApi('/pdfs/stats');
@@ -228,19 +228,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
228
228
  if (!params.pdfUrl.startsWith('/api/pdfs/')) {
229
229
  result = {
230
230
  success: false,
231
- message: `Invalid pdfUrl format: "${params.pdfUrl}". You must pass the exact 'pdfUrl' value returned by get_company_roles (e.g. /api/pdfs/secure/<id>).`
231
+ message: `Invalid pdfUrl format: "${params.pdfUrl}". You must pass the exact 'pdfUrl' value returned by get_company_roles (e.g. /api/pdfs/view/<token>).`
232
232
  };
233
233
  break;
234
234
  }
235
- // pdfUrl from the backend is a relative path like /api/pdfs/secure/:id
235
+ // pdfUrl from the backend is a relative path like /api/pdfs/view/:token
236
236
  // invokeApiBinary prepends API_BASE_URL which already ends in /api,
237
237
  // so we must strip the leading /api prefix to avoid doubling it.
238
238
  const normalizedPdfUrl = params.pdfUrl.replace(/^\/api/, '');
239
- const arrayBuffer = await invokeApiBinary(normalizedPdfUrl);
239
+ let binaryEndpoint = normalizedPdfUrl;
240
+
241
+ // New flow: redeem token and download through viewer session document endpoint.
242
+ if (/^\/pdfs\/view\/.+/.test(normalizedPdfUrl)) {
243
+ const viewPayload = await invokeApi(normalizedPdfUrl, 'GET');
244
+ const sessionToken = viewPayload?.viewer?.sessionToken;
245
+ if (!sessionToken) {
246
+ throw new Error('Viewer session was not created for this pdfUrl.');
247
+ }
248
+ binaryEndpoint = `/pdfs/viewer/${sessionToken}/document`;
249
+ }
250
+
251
+ const arrayBuffer = await invokeApiBinary(binaryEndpoint);
240
252
  const buffer = Buffer.from(arrayBuffer);
241
253
  const pdfData = await pdfParse(buffer);
242
- result = {
243
- success: true,
254
+ result = {
255
+ success: true,
244
256
  text: pdfData.text,
245
257
  pages: pdfData.numpages,
246
258
  message: "PDF read successfully."