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.
- package/adapters/apiClient.js +6 -6
- package/package.json +1 -1
- package/server.js +35 -23
package/adapters/apiClient.js
CHANGED
|
@@ -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
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: "
|
|
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: `
|
|
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
|
|
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
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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
|
|
258
|
+
message: "PDF read successfully."
|
|
247
259
|
};
|
|
248
260
|
break;
|
|
249
261
|
}
|