whistler-mcp 1.0.7 → 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.7",
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.",
@@ -109,7 +109,7 @@ const toolsDefinition = {
109
109
 
110
110
  // Read PDF
111
111
  view_job_pdf: {
112
- description: "Read the specific textual content of a company's job role PDF. This will consume 1 from the user's daily PDF view limit. Ensure you pass the exact `pdfUrl` obtained from `get_company_roles`.",
112
+ description: "Extract and read the textual content of a company's job role PDF. (Consumes 1 daily view).",
113
113
  schema: z.object({ pdfUrl: z.string() }),
114
114
  inputSchema: { type: "object", properties: { pdfUrl: { type: "string" } }, required: ["pdfUrl"] }
115
115
  }
@@ -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,21 +155,21 @@ 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
- result = { success: true, message: `Successfully authenticated! Token saved securely.` };
172
+ result = { success: true, message: `Authenticated!` };
173
173
  break;
174
174
  }
175
175
  case 'signup_to_whistler': {
@@ -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
  }
@@ -191,18 +191,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
191
191
  if (!signupData.token) throw new Error('Signup succeeded but no token was returned.');
192
192
 
193
193
  saveToken(signupData.token, signupData);
194
- result = { success: true, message: `Account created and authenticated successfully! Welcome to Whistler.` };
194
+ result = { success: true, message: `Account created!` };
195
195
  break;
196
196
  }
197
197
  case 'logout_whistler':
198
198
  clearToken();
199
- result = { success: true, message: 'Logged out successfully. Token cleared.' };
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,22 +228,34 @@ 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
- message: "PDF successfully read and your daily limit has been updated."
258
+ message: "PDF read successfully."
247
259
  };
248
260
  break;
249
261
  }