techprufer-mcp 0.4.0 → 0.4.3

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/dist/api.d.ts CHANGED
@@ -5,6 +5,7 @@ export declare class ApiError extends Error {
5
5
  constructor(status: number, body: unknown, message?: string);
6
6
  }
7
7
  export declare function setActiveClientInfo(info: ClientInfo | null): void;
8
+ export declare function setClientInfoSync(fn: (() => ClientInfo | null | undefined) | null): void;
8
9
  export declare function getActiveClientInfo(): ClientInfo | null;
9
10
  export declare function apiFetch<T>(path: string, init?: RequestInit & {
10
11
  token?: string | null;
package/dist/api.js CHANGED
@@ -22,12 +22,28 @@ function formatFetchError(err) {
22
22
  }
23
23
  /** Set by the stdio server after the MCP initialize handshake. */
24
24
  let activeClientInfo = null;
25
+ /** Re-read MCP clientInfo from the SDK before each API call (hosts may omit it until later). */
26
+ let clientInfoSync = null;
25
27
  export function setActiveClientInfo(info) {
26
28
  activeClientInfo = info;
27
29
  }
30
+ export function setClientInfoSync(fn) {
31
+ clientInfoSync = fn;
32
+ }
28
33
  export function getActiveClientInfo() {
29
34
  return activeClientInfo;
30
35
  }
36
+ function refreshActiveClientInfo() {
37
+ if (!clientInfoSync)
38
+ return;
39
+ const info = clientInfoSync();
40
+ if (info?.name?.trim()) {
41
+ setActiveClientInfo({
42
+ name: info.name.trim(),
43
+ version: info.version?.trim() || undefined,
44
+ });
45
+ }
46
+ }
31
47
  export async function apiFetch(path, init = {}) {
32
48
  const creds = loadCredentials();
33
49
  const token = init.token === undefined ? creds?.token : init.token;
@@ -45,6 +61,7 @@ export async function apiFetch(path, init = {}) {
45
61
  }
46
62
  if (token)
47
63
  headers.set('Authorization', `Bearer ${token}`);
64
+ refreshActiveClientInfo();
48
65
  const clientHeader = formatClientHeader(activeClientInfo);
49
66
  if (clientHeader)
50
67
  headers.set(TP_CLIENT_HEADER, clientHeader);
package/dist/cli.js CHANGED
@@ -46,11 +46,13 @@ async function main() {
46
46
  const cmd = args[0];
47
47
  if (cmd === 'login') {
48
48
  const nameIdx = args.indexOf('--name');
49
- const deviceName = nameIdx >= 0 ? args[nameIdx + 1] : undefined;
49
+ const explicitDeviceName = nameIdx >= 0 ? args[nameIdx + 1] : undefined;
50
50
  const skipSetup = hasFlag(args, '--no-setup');
51
51
  const timeoutSec = parseTimeoutSec(args);
52
52
  const tools = parseToolsFlag(args);
53
- let configuredIds = [];
53
+ let configuredIds = tools?.length
54
+ ? tools
55
+ : [];
54
56
  if (!skipSetup) {
55
57
  const outcome = await runSetup({
56
58
  beforeLogin: true,
@@ -65,6 +67,7 @@ async function main() {
65
67
  return;
66
68
  console.error('');
67
69
  }
70
+ const deviceName = explicitDeviceName ?? configuredIds[0];
68
71
  await runLogin(deviceName, {
69
72
  stepLabel: skipSetup ? undefined : 'login',
70
73
  timeoutSec,
package/dist/server.js CHANGED
@@ -1,13 +1,20 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { z } from 'zod';
4
- import { apiFetch, ApiError, setActiveClientInfo } from './api.js';
4
+ import { apiFetch, ApiError, setActiveClientInfo, setClientInfoSync } from './api.js';
5
5
  import { brandIcons } from './brand.js';
6
6
  import { loadCredentials, credentialsFilePath } from './config.js';
7
7
  import { runLogin } from './login.js';
8
8
  function textResult(text, isError = false) {
9
9
  return { content: [{ type: 'text', text }], isError };
10
10
  }
11
+ /** Prefer API `markdown` field; fall back to JSON for structured payloads. */
12
+ function apiResult(data, isError = false) {
13
+ const md = typeof data.markdown === 'string' ? data.markdown : null;
14
+ if (md)
15
+ return textResult(md, isError);
16
+ return textResult(JSON.stringify(data, null, 2), isError);
17
+ }
11
18
  function requireAuthMessage() {
12
19
  return (`Not logged in. Run: npx techprufer-mcp login\n` +
13
20
  `Credentials file: ${credentialsFilePath()}\n` +
@@ -21,18 +28,25 @@ export function createServer() {
21
28
  const server = new McpServer({
22
29
  name: 'techprufer',
23
30
  title: 'TechPrufer',
24
- version: '0.4.0',
31
+ version: '0.4.3',
25
32
  description: 'Tailor resumes and build profiles with your AI tool — no Gemini API key.',
26
33
  websiteUrl: 'https://techprufer.com',
27
34
  icons: brandIcons(),
28
35
  });
29
- server.tool('list_pending_tailor_jobs', 'List TechPrufer tailor jobs queued for MCP. Returns id, company, role, status.', {}, async () => {
36
+ server.tool('list_pending_tailor_jobs', 'List queued MCP tailor jobs. Always includes Prepare-stage applications needing a resume ' +
37
+ '(prepareSummary + prepareApplications) so when the queue is empty you can show the user ' +
38
+ 'their Prepare pipeline backlog and proceed to enqueue_tailor_job / company picker.', {}, async () => {
30
39
  const token = await ensureToken();
31
40
  if (!token)
32
41
  return textResult(requireAuthMessage(), true);
33
42
  try {
34
43
  const data = await apiFetch('/api/agent/jobs');
35
- return textResult(JSON.stringify(data.jobs ?? [], null, 2));
44
+ return textResult(JSON.stringify({
45
+ jobs: data.jobs ?? [],
46
+ prepareSummary: data.prepareSummary ?? null,
47
+ prepareApplications: data.prepareApplications ?? [],
48
+ prepareTotal: data.prepareTotal ?? 0,
49
+ }, null, 2));
36
50
  }
37
51
  catch (e) {
38
52
  const msg = e instanceof Error ? e.message : String(e);
@@ -124,14 +138,21 @@ export function createServer() {
124
138
  '### Flow',
125
139
  jobId
126
140
  ? `1) Process jobId=${jobId}: get_tailor_job → execute → submit_tailor_result.`
127
- : '1) Call list_pending_tailor_jobs. Show company/role for each. Ask: process them now, or SKIP to company picker? If process: ONE AT A TIME (get_tailor_job → execute → submit_tailor_result; fix once on correctiveMessage).',
141
+ : [
142
+ '1) Call list_pending_tailor_jobs.',
143
+ ' - If jobs[] is non-empty: show company/role for each queued job. Ask: process them now, or SKIP to Prepare picker?',
144
+ ' If process: ONE AT A TIME (get_tailor_job → execute → submit_tailor_result; fix once on correctiveMessage).',
145
+ ' - If jobs[] is empty: show prepareSummary (totalPrepare / needingTailor) and prepareApplications',
146
+ ' (company, role, dashboardUrl for each Prepare-stage role still missing a tailored resume).',
147
+ ' Ask which to tailor, or SKIP to the company picker (step 4).',
148
+ ].join('\n'),
128
149
  applicationId
129
150
  ? `2) applicationId=${applicationId}: enqueue_tailor_job({ applicationId }) then process, OR get_resume(kind=tailored) to edit/delete.`
130
151
  : '2) If user gave an application id: enqueue_tailor_job or edit/delete that app.',
131
152
  profileId
132
153
  ? `3) profileId=${profileId}: get_resume(kind=base) → update_resume / delete_resume.`
133
154
  : '3) If user gave a base profile id: edit/delete that base resume.',
134
- '4) Company picker (when skipping pending / no id):',
155
+ '4) Company picker (when skipping pending / no id / user picks from Prepare list):',
135
156
  ' - list_tracked_companies (top 10, paginate, or query= typed name). Show total / withTailored / withoutTailored.',
136
157
  ' - Ask which company (or they type the name).',
137
158
  ' - list_applications_at_company(company, onlyUntailored=true preferred for new tailor). If multiple roles, list roles and ask which.',
@@ -141,8 +162,9 @@ export function createServer() {
141
162
  ' - Tell the user: this company has N other tracked roles — attach this resume to them?',
142
163
  ' - If yes: preview_company_siblings then attach_tailored_to_siblings({ sourceApplicationId, sessionId }).',
143
164
  ' - If no: leave siblings alone.',
144
- '6) Always show the user from submit result: jdUrl (job link), resumePdfUrl, dashboardUrl.',
145
- ' On that job page the Chrome extension can inject or download the PDF when resumePdfUrl is set.',
165
+ '6) Always show the user from submit result: jdUrl (job link), dashboardUrl.',
166
+ ' resumePdfUrl is an on-demand API URL (requires login) never show /generated/ paths.',
167
+ ' On the dashboard application page the Chrome extension can inject or download the PDF.',
146
168
  '',
147
169
  'Zero hallucination. Complete JSON only when submitting or saving.',
148
170
  ].join('\n'),
@@ -150,6 +172,73 @@ export function createServer() {
150
172
  },
151
173
  ],
152
174
  }));
175
+ server.tool('list_profile_templates', 'List all 6 TechPrufer resume templates with descriptions, tiers, focus order, and boosts. ' +
176
+ 'Use when the user wants to compare template options during build-profile.', {}, async () => {
177
+ const token = await ensureToken();
178
+ if (!token)
179
+ return textResult(requireAuthMessage(), true);
180
+ try {
181
+ const data = await apiFetch('/api/agent/templates');
182
+ return apiResult(data);
183
+ }
184
+ catch (e) {
185
+ const msg = e instanceof Error ? e.message : String(e);
186
+ return textResult(`Failed to list templates: ${msg}`, true);
187
+ }
188
+ });
189
+ server.tool('recommend_profile_template', 'Analyze resume text and recommend the best of 6 templates, with ranked alternatives and a comparison guide. ' +
190
+ 'Run after extracting text from a PDF the user attached in chat.', {
191
+ resumeText: z.string().describe('Full plain text extracted from the resume PDF'),
192
+ }, async ({ resumeText }) => {
193
+ const token = await ensureToken();
194
+ if (!token)
195
+ return textResult(requireAuthMessage(), true);
196
+ try {
197
+ const data = await apiFetch('/api/agent/templates/recommend', {
198
+ method: 'POST',
199
+ body: JSON.stringify({ resumeText }),
200
+ });
201
+ return apiResult(data);
202
+ }
203
+ catch (e) {
204
+ const msg = e instanceof Error ? e.message : String(e);
205
+ return textResult(`Template recommendation failed: ${msg}`, true);
206
+ }
207
+ });
208
+ server.tool('analyze_profile_job', 'Analyze a queued site PDF job for template recommendation without claiming it. ' +
209
+ 'Use before get_profile_job when templateSlug is not set.', {
210
+ jobId: z.string().describe('Profile job id from list_pending_profile_jobs'),
211
+ }, async ({ jobId }) => {
212
+ const token = await ensureToken();
213
+ if (!token)
214
+ return textResult(requireAuthMessage(), true);
215
+ try {
216
+ const data = await apiFetch(`/api/agent/profile-jobs/${encodeURIComponent(jobId)}/analyze`);
217
+ return apiResult(data);
218
+ }
219
+ catch (e) {
220
+ const msg = e instanceof Error ? e.message : String(e);
221
+ return textResult(`Analyze job failed: ${msg}`, true);
222
+ }
223
+ });
224
+ server.tool('set_profile_job_template', 'Set the gallery template slug on a queued profile job before parsing/build.', {
225
+ jobId: z.string(),
226
+ templateSlug: z
227
+ .string()
228
+ .describe('Slug from list_profile_templates / recommend_profile_template (e.g. student-classic)'),
229
+ }, async ({ jobId, templateSlug }) => {
230
+ const token = await ensureToken();
231
+ if (!token)
232
+ return textResult(requireAuthMessage(), true);
233
+ try {
234
+ const data = await apiFetch(`/api/agent/profile-jobs/${encodeURIComponent(jobId)}/template`, { method: 'PATCH', body: JSON.stringify({ templateSlug }) });
235
+ return textResult(JSON.stringify(data, null, 2));
236
+ }
237
+ catch (e) {
238
+ const msg = e instanceof Error ? e.message : String(e);
239
+ return textResult(`Set template failed: ${msg}`, true);
240
+ }
241
+ });
153
242
  server.tool('list_pending_profile_jobs', 'List TechPrufer profile-build jobs queued for MCP (from PDF upload on the site).', {}, async () => {
154
243
  const token = await ensureToken();
155
244
  if (!token)
@@ -184,7 +273,7 @@ export function createServer() {
184
273
  id = first.id;
185
274
  }
186
275
  const payload = await apiFetch(`/api/agent/profile-jobs/${encodeURIComponent(id)}/payload`);
187
- return textResult(JSON.stringify(payload, null, 2));
276
+ return apiResult(payload);
188
277
  }
189
278
  catch (e) {
190
279
  const msg = e instanceof Error ? e.message : String(e);
@@ -239,7 +328,11 @@ export function createServer() {
239
328
  name: z.string().optional().describe('Profile display name'),
240
329
  isDefault: z.boolean().optional(),
241
330
  model: z.string().optional(),
242
- }, async ({ profileJson, name, isDefault, model }) => {
331
+ templateSlug: z
332
+ .string()
333
+ .optional()
334
+ .describe('Gallery template slug chosen by the user (from recommend_profile_template)'),
335
+ }, async ({ profileJson, name, isDefault, model, templateSlug }) => {
243
336
  const token = await ensureToken();
244
337
  if (!token)
245
338
  return textResult(requireAuthMessage(), true);
@@ -247,7 +340,7 @@ export function createServer() {
247
340
  const parsed = typeof profileJson === 'string' ? JSON.parse(profileJson) : profileJson;
248
341
  const result = await apiFetch('/api/agent/profiles', {
249
342
  method: 'POST',
250
- body: JSON.stringify({ profileJson: parsed, name, isDefault, model }),
343
+ body: JSON.stringify({ profileJson: parsed, name, isDefault, model, templateSlug }),
251
344
  });
252
345
  return textResult(JSON.stringify(result, null, 2));
253
346
  }
@@ -512,20 +605,30 @@ export function createServer() {
512
605
  text: [
513
606
  'Build TechPrufer master profile(s) via MCP.',
514
607
  '',
608
+ 'TEMPLATE STEP (always before parsing — PDF in chat or queued job):',
609
+ '1. Extract full resume text (from attached PDF or analyze_profile_job for site queue).',
610
+ '2. Call recommend_profile_template(resumeText) OR analyze_profile_job(jobId).',
611
+ '3. Tell the user: show the **markdown** from the tool response (recommended template + ask if they want other options).',
612
+ '4. Ask: "Want to see other template versions?"',
613
+ ' - If yes: the markdown already includes ranked alternatives and a comparison section — walk through the differences.',
614
+ '5. Wait for the user to pick a template slug.',
615
+ ' - Queued job: set_profile_job_template(jobId, slug) before get_profile_job.',
616
+ ' - Local PDF: pass templateSlug to submit_built_profile.',
617
+ '',
515
618
  'Path A — queued PDF jobs from the site:',
516
619
  jobId
517
- ? `1. Call get_profile_job with jobId=${jobId}.`
518
- : '1. Call list_pending_profile_jobs; for each QUEUED/CLAIMED job call get_profile_job.',
620
+ ? `1. analyze_profile_job(${jobId}) template pick → set_profile_job_template → get_profile_job.`
621
+ : '1. list_pending_profile_jobs analyze_profile_job template pick → set_profile_job_template → get_profile_job.',
519
622
  '2. Execute returned messages verbatim; output ONLY CPM profile JSON.',
520
623
  '3. Call submit_profile_job_result. On correctiveMessage, fix once and resubmit.',
521
624
  '',
522
- 'Path B — local resume file (user selected / open in this tool):',
523
- '1. Read the resume file and extract full text.',
524
- '2. Call get_profile_build_messages with that resumeText.',
525
- '3. Execute messages verbatim; output ONLY CPM profile JSON.',
526
- '4. Call submit_built_profile (optional name from file / identity.name).',
625
+ 'Path B — PDF/resume attached in this chat:',
626
+ '1. Read/extract full text from the attached PDF.',
627
+ '2. recommend_profile_template user picks template (steps above).',
628
+ '3. get_profile_build_messages(resumeText) execute verbatim CPM JSON only.',
629
+ '4. submit_built_profile with templateSlug + optional name.',
527
630
  '',
528
- 'Prefer Path A if pending jobs exist; otherwise Path B when a local resume is available.',
631
+ 'Prefer Path A if pending jobs exist; otherwise Path B when user attached a PDF.',
529
632
  'Zero hallucination. Complete JSON only. Faithful extraction, not rewriting.',
530
633
  ].join('\n'),
531
634
  },
@@ -536,15 +639,18 @@ export function createServer() {
536
639
  }
537
640
  export async function startStdioServer() {
538
641
  const server = createServer();
642
+ const syncClientInfo = () => {
643
+ const info = server.server.getClientVersion();
644
+ if (!info?.name?.trim())
645
+ return null;
646
+ return { name: info.name.trim(), version: info.version?.trim() || undefined };
647
+ };
539
648
  // Capture IDE identity from the MCP initialize handshake (not env vars).
649
+ setClientInfoSync(syncClientInfo);
540
650
  server.server.oninitialized = () => {
541
- const info = server.server.getClientVersion();
542
- if (info?.name) {
543
- setActiveClientInfo({
544
- name: info.name,
545
- version: info.version,
546
- });
547
- }
651
+ const info = syncClientInfo();
652
+ if (info)
653
+ setActiveClientInfo(info);
548
654
  };
549
655
  const transport = new StdioServerTransport();
550
656
  await server.connect(transport);
package/dist/tools.js CHANGED
@@ -164,29 +164,29 @@ export function codexPromptsDir() {
164
164
  const CODEX_TAILOR_PROMPT = [
165
165
  'TechPrufer MCP — all resume control under /techprufer-tailor.',
166
166
  '',
167
- '1) list_pending_tailor_jobs — show them; ask process now or SKIP to company picker.',
167
+ '1) list_pending_tailor_jobs — if jobs[]: offer to process; if empty: show prepareSummary + prepareApplications (Prepare pipeline roles needing a resume).',
168
168
  '2) applicationId / profileId: enqueue or edit/delete.',
169
169
  '3) Company picker: list_tracked_companies (top 10 / query) → list_applications_at_company → enqueue_tailor_job → process.',
170
- '4) After submit_tailor_result: show jdUrl + resumePdfUrl. If siblingCount>0, ask to attach to other roles (preview_company_siblings → attach_tailored_to_siblings).',
170
+ '4) After submit_tailor_result: show jdUrl + dashboardUrl. resumePdfUrl only if present (API URL, not /generated/). If siblingCount>0, ask to attach to other roles (preview_company_siblings → attach_tailored_to_siblings).',
171
171
  '',
172
172
  'Zero hallucination. Complete JSON only when submitting or saving.',
173
173
  '',
174
174
  ].join('\n');
175
175
  const CODEX_BUILD_PROFILE_PROMPT = [
176
- 'Process TechPrufer MCP profile-build jobs (or build from a local resume).',
176
+ 'Process TechPrufer MCP profile-build jobs (or build from a PDF/resume in chat).',
177
+ '',
178
+ 'TEMPLATE STEP (before parsing):',
179
+ '1. Extract resume text (PDF attachment or analyze_profile_job for site queue).',
180
+ '2. recommend_profile_template → show the markdown to the user (recommended + ask if they want other options).',
181
+ '3. If yes: markdown includes ranked alternatives and comparison — explain differences.',
182
+ '4. User picks slug → set_profile_job_template (queued) or templateSlug on submit_built_profile (local).',
177
183
  '',
178
184
  'Path A — queued PDF jobs from the site:',
179
- '1. Call list_pending_profile_jobs; for each QUEUED/CLAIMED job call get_profile_job.',
180
- '2. Execute returned messages verbatim; output ONLY CPM profile JSON.',
181
- '3. Call submit_profile_job_result. On correctiveMessage, fix once and resubmit.',
185
+ 'analyze_profile_job template pick set_profile_job_template get_profile_job → submit_profile_job_result.',
182
186
  '',
183
- 'Path B — local resume file (user selected / open in this tool):',
184
- '1. Read the resume file and extract full text.',
185
- '2. Call get_profile_build_messages with that resumeText.',
186
- '3. Execute messages verbatim; output ONLY CPM profile JSON.',
187
- '4. Call submit_built_profile (optional name from file / identity.name).',
187
+ 'Path B — PDF attached in chat:',
188
+ 'extract text recommend_profile_template user picks get_profile_build_messages → submit_built_profile(templateSlug).',
188
189
  '',
189
- 'Prefer Path A if pending jobs exist; otherwise Path B when a local resume is available.',
190
190
  'Zero hallucination. Complete JSON only. Faithful extraction, not rewriting.',
191
191
  '',
192
192
  ].join('\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "techprufer-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.4.3",
4
4
  "description": "TechPrufer MCP — tailor resumes and build profiles from your AI tool",
5
5
  "homepage": "https://techprufer.com",
6
6
  "type": "module",
@@ -15,6 +15,11 @@
15
15
  "engines": {
16
16
  "node": ">=18"
17
17
  },
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "test": "npm run build && node --test dist/*.test.js",
21
+ "prepublishOnly": "npm run build"
22
+ },
18
23
  "dependencies": {
19
24
  "@modelcontextprotocol/sdk": "^1.12.1",
20
25
  "zod": "^3.24.2"
@@ -29,9 +34,5 @@
29
34
  "resume",
30
35
  "tailor"
31
36
  ],
32
- "license": "MIT",
33
- "scripts": {
34
- "build": "tsc -p tsconfig.json",
35
- "test": "npm run build && node --test dist/*.test.js"
36
- }
37
- }
37
+ "license": "MIT"
38
+ }