techprufer-mcp 0.1.0 → 0.1.5
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/README.md +15 -14
- package/assets/logo-48.png +0 -0
- package/assets/logo.png +0 -0
- package/dist/brand.d.ts +6 -0
- package/dist/brand.js +21 -0
- package/dist/cli.js +1 -1
- package/dist/login.js +17 -3
- package/dist/server.js +151 -4
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# techprufer-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
TechPrufer MCP — tailor resumes and build profiles from your AI tool.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Uses the same TechPrufer cube mark as the site and Chrome extension (shown in MCP clients that support server icons).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Server key **must** be `techprufer` so slash commands are `/techprufer/…`:
|
|
6
10
|
|
|
7
11
|
```json
|
|
8
12
|
{
|
|
@@ -15,26 +19,23 @@ Local Agent (MCP) for [TechPrufer](https://techprufer.com) — tailor resumes fr
|
|
|
15
19
|
}
|
|
16
20
|
```
|
|
17
21
|
|
|
18
|
-
## Login
|
|
22
|
+
## Login
|
|
19
23
|
|
|
20
24
|
```bash
|
|
21
25
|
npx techprufer-mcp login
|
|
22
26
|
```
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
## Use
|
|
28
|
+
## Slash commands
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
2. In your AI tool, run the `/tailor` prompt (or ask: "process my pending TechPrufer tailor jobs").
|
|
30
|
-
3. The agent pulls jobs one at a time, generates the tailored JSON with its own model, and saves it back after server-side validation.
|
|
30
|
+
Type `/techprufer` in your AI tool to list:
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
| Command | Use |
|
|
33
|
+
|---------|-----|
|
|
34
|
+
| `/techprufer/tailor` | Process queued tailor jobs from the site |
|
|
35
|
+
| `/techprufer/build-profile` | Process queued PDF→profile jobs, or build from a local resume |
|
|
33
36
|
|
|
34
|
-
-
|
|
35
|
-
- `get_tailor_job` — claim + exact prompt payload
|
|
36
|
-
- `submit_tailor_result` — validate + store (retries with corrective feedback)
|
|
37
|
+
> Cursor may show `/user-techprufer/…` for user-scoped installs; same commands.
|
|
37
38
|
|
|
38
39
|
## Env
|
|
39
40
|
|
|
40
|
-
- `TECHPRUFER_API_URL` —
|
|
41
|
+
- `TECHPRUFER_API_URL` — API base (default `https://techprufer.com`)
|
|
Binary file
|
package/assets/logo.png
ADDED
|
Binary file
|
package/dist/brand.d.ts
ADDED
package/dist/brand.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
5
|
+
/** TechPrufer cube mark (same asset as the Chrome extension / site brand). */
|
|
6
|
+
export function brandIcons() {
|
|
7
|
+
const png128 = readFileSync(join(PACKAGE_ROOT, 'assets/logo.png'));
|
|
8
|
+
const png48 = readFileSync(join(PACKAGE_ROOT, 'assets/logo-48.png'));
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
src: `data:image/png;base64,${png128.toString('base64')}`,
|
|
12
|
+
mimeType: 'image/png',
|
|
13
|
+
sizes: ['128x128'],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
src: `data:image/png;base64,${png48.toString('base64')}`,
|
|
17
|
+
mimeType: 'image/png',
|
|
18
|
+
sizes: ['48x48'],
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -23,7 +23,7 @@ async function main() {
|
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
26
|
-
console.error(`techprufer-mcp — TechPrufer
|
|
26
|
+
console.error(`techprufer-mcp — TechPrufer MCP
|
|
27
27
|
|
|
28
28
|
Usage:
|
|
29
29
|
npx techprufer-mcp Start stdio MCP server (for AI tools)
|
package/dist/login.js
CHANGED
|
@@ -15,20 +15,34 @@ function openBrowser(url) {
|
|
|
15
15
|
function sleep(ms) {
|
|
16
16
|
return new Promise((r) => setTimeout(r, ms));
|
|
17
17
|
}
|
|
18
|
+
/** Keep verify URL on the same host as TECHPRUFER_API_URL (local vs prod). */
|
|
19
|
+
function alignVerifyUrl(verifyUrl, apiUrl) {
|
|
20
|
+
try {
|
|
21
|
+
const verify = new URL(verifyUrl);
|
|
22
|
+
const api = new URL(apiUrl);
|
|
23
|
+
verify.protocol = api.protocol;
|
|
24
|
+
verify.host = api.host;
|
|
25
|
+
return verify.toString();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return verifyUrl;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
18
31
|
export async function runLogin(deviceName) {
|
|
19
32
|
const apiUrl = getApiUrl();
|
|
20
|
-
console.error(`TechPrufer
|
|
33
|
+
console.error(`TechPrufer MCP — logging in against ${apiUrl}`);
|
|
21
34
|
const start = await apiFetch('/api/agent/device/start', {
|
|
22
35
|
method: 'POST',
|
|
23
36
|
token: null,
|
|
24
37
|
body: JSON.stringify({ deviceName: deviceName || undefined }),
|
|
25
38
|
});
|
|
39
|
+
const verifyUrl = alignVerifyUrl(start.verifyUrl, apiUrl);
|
|
26
40
|
console.error('');
|
|
27
41
|
console.error(` Code: ${start.userCode}`);
|
|
28
|
-
console.error(` Open: ${
|
|
42
|
+
console.error(` Open: ${verifyUrl}`);
|
|
29
43
|
console.error('');
|
|
30
44
|
console.error('Waiting for approval in the browser…');
|
|
31
|
-
openBrowser(
|
|
45
|
+
openBrowser(verifyUrl);
|
|
32
46
|
const intervalMs = Math.max(3, start.interval || 5) * 1000;
|
|
33
47
|
const deadline = Date.now() + start.expiresIn * 1000;
|
|
34
48
|
while (Date.now() < deadline) {
|
package/dist/server.js
CHANGED
|
@@ -2,6 +2,7 @@ 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
4
|
import { apiFetch, ApiError } from './api.js';
|
|
5
|
+
import { brandIcons } from './brand.js';
|
|
5
6
|
import { loadCredentials, credentialsFilePath } from './config.js';
|
|
6
7
|
import { runLogin } from './login.js';
|
|
7
8
|
function textResult(text, isError = false) {
|
|
@@ -19,9 +20,13 @@ async function ensureToken() {
|
|
|
19
20
|
export function createServer() {
|
|
20
21
|
const server = new McpServer({
|
|
21
22
|
name: 'techprufer',
|
|
22
|
-
|
|
23
|
+
title: 'TechPrufer',
|
|
24
|
+
version: '0.1.5',
|
|
25
|
+
description: 'Tailor resumes and build profiles with your AI tool — no Gemini API key.',
|
|
26
|
+
websiteUrl: 'https://techprufer.com',
|
|
27
|
+
icons: brandIcons(),
|
|
23
28
|
});
|
|
24
|
-
server.tool('list_pending_tailor_jobs', 'List TechPrufer tailor jobs queued for
|
|
29
|
+
server.tool('list_pending_tailor_jobs', 'List TechPrufer tailor jobs queued for MCP. Returns id, company, role, status.', {}, async () => {
|
|
25
30
|
const token = await ensureToken();
|
|
26
31
|
if (!token)
|
|
27
32
|
return textResult(requireAuthMessage(), true);
|
|
@@ -93,7 +98,8 @@ export function createServer() {
|
|
|
93
98
|
return textResult(`Submit failed: ${msg}`, true);
|
|
94
99
|
}
|
|
95
100
|
});
|
|
96
|
-
|
|
101
|
+
// Slash path in Cursor: /techprufer/tailor (user installs may show /user-techprufer/tailor)
|
|
102
|
+
server.prompt('tailor', 'TechPrufer — tailor a queued resume job', {
|
|
97
103
|
jobId: z
|
|
98
104
|
.string()
|
|
99
105
|
.optional()
|
|
@@ -105,7 +111,7 @@ export function createServer() {
|
|
|
105
111
|
content: {
|
|
106
112
|
type: 'text',
|
|
107
113
|
text: [
|
|
108
|
-
'Process TechPrufer
|
|
114
|
+
'Process TechPrufer MCP tailor jobs.',
|
|
109
115
|
jobId
|
|
110
116
|
? `Focus on jobId=${jobId}.`
|
|
111
117
|
: 'Fetch pending jobs and process them ONE AT A TIME in order.',
|
|
@@ -124,6 +130,147 @@ export function createServer() {
|
|
|
124
130
|
},
|
|
125
131
|
],
|
|
126
132
|
}));
|
|
133
|
+
server.tool('list_pending_profile_jobs', 'List TechPrufer profile-build jobs queued for MCP (from PDF upload on the site).', {}, async () => {
|
|
134
|
+
const token = await ensureToken();
|
|
135
|
+
if (!token)
|
|
136
|
+
return textResult(requireAuthMessage(), true);
|
|
137
|
+
try {
|
|
138
|
+
const data = await apiFetch('/api/agent/profile-jobs');
|
|
139
|
+
return textResult(JSON.stringify(data.jobs ?? [], null, 2));
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
143
|
+
return textResult(`Failed to list profile jobs: ${msg}`, true);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
server.tool('get_profile_job', 'Claim a pending profile-build job and return the EXACT CPM parse prompt messages (same as Gemini parse). ' +
|
|
147
|
+
'Execute verbatim, then call submit_profile_job_result.', {
|
|
148
|
+
jobId: z
|
|
149
|
+
.string()
|
|
150
|
+
.optional()
|
|
151
|
+
.describe('Job id from list_pending_profile_jobs. If omitted, oldest QUEUED job.'),
|
|
152
|
+
}, async ({ jobId }) => {
|
|
153
|
+
const token = await ensureToken();
|
|
154
|
+
if (!token)
|
|
155
|
+
return textResult(requireAuthMessage(), true);
|
|
156
|
+
try {
|
|
157
|
+
let id = jobId;
|
|
158
|
+
if (!id) {
|
|
159
|
+
const list = await apiFetch('/api/agent/profile-jobs');
|
|
160
|
+
const first = list.jobs.find((j) => j.status === 'QUEUED') ??
|
|
161
|
+
list.jobs.find((j) => j.status === 'CLAIMED');
|
|
162
|
+
if (!first)
|
|
163
|
+
return textResult('No pending profile jobs.');
|
|
164
|
+
id = first.id;
|
|
165
|
+
}
|
|
166
|
+
const payload = await apiFetch(`/api/agent/profile-jobs/${encodeURIComponent(id)}/payload`);
|
|
167
|
+
return textResult(JSON.stringify(payload, null, 2));
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
171
|
+
return textResult(`Failed to get profile job: ${msg}`, true);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
server.tool('submit_profile_job_result', 'Submit CPM profile JSON for a queued profile job. On 422, fix via correctiveMessage and resubmit.', {
|
|
175
|
+
jobId: z.string(),
|
|
176
|
+
profileJson: z.union([z.string(), z.record(z.unknown())]),
|
|
177
|
+
model: z.string().optional(),
|
|
178
|
+
}, async ({ jobId, profileJson, model }) => {
|
|
179
|
+
const token = await ensureToken();
|
|
180
|
+
if (!token)
|
|
181
|
+
return textResult(requireAuthMessage(), true);
|
|
182
|
+
try {
|
|
183
|
+
const parsed = typeof profileJson === 'string' ? JSON.parse(profileJson) : profileJson;
|
|
184
|
+
const result = await apiFetch(`/api/agent/profile-jobs/${encodeURIComponent(jobId)}/result`, {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
body: JSON.stringify({ profileJson: parsed, model }),
|
|
187
|
+
});
|
|
188
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
if (e instanceof ApiError && e.status === 422) {
|
|
192
|
+
return textResult(JSON.stringify(e.body, null, 2), true);
|
|
193
|
+
}
|
|
194
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
195
|
+
return textResult(`Submit failed: ${msg}`, true);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
server.tool('get_profile_build_messages', 'For a local resume file: pass extracted resume text; returns the EXACT CPM parse messages (no queue). ' +
|
|
199
|
+
'Execute verbatim, then call submit_built_profile.', {
|
|
200
|
+
resumeText: z.string().describe('Full text of the resume (read from the local file)'),
|
|
201
|
+
}, async ({ resumeText }) => {
|
|
202
|
+
const token = await ensureToken();
|
|
203
|
+
if (!token)
|
|
204
|
+
return textResult(requireAuthMessage(), true);
|
|
205
|
+
try {
|
|
206
|
+
const payload = await apiFetch('/api/agent/profiles/messages', {
|
|
207
|
+
method: 'POST',
|
|
208
|
+
body: JSON.stringify({ resumeText }),
|
|
209
|
+
});
|
|
210
|
+
return textResult(JSON.stringify(payload, null, 2));
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
214
|
+
return textResult(`Failed to get messages: ${msg}`, true);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
server.tool('submit_built_profile', 'Create a TechPrufer master profile from CPM JSON built from a local resume (no queued job).', {
|
|
218
|
+
profileJson: z.union([z.string(), z.record(z.unknown())]),
|
|
219
|
+
name: z.string().optional().describe('Profile display name'),
|
|
220
|
+
isDefault: z.boolean().optional(),
|
|
221
|
+
model: z.string().optional(),
|
|
222
|
+
}, async ({ profileJson, name, isDefault, model }) => {
|
|
223
|
+
const token = await ensureToken();
|
|
224
|
+
if (!token)
|
|
225
|
+
return textResult(requireAuthMessage(), true);
|
|
226
|
+
try {
|
|
227
|
+
const parsed = typeof profileJson === 'string' ? JSON.parse(profileJson) : profileJson;
|
|
228
|
+
const result = await apiFetch('/api/agent/profiles', {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
body: JSON.stringify({ profileJson: parsed, name, isDefault, model }),
|
|
231
|
+
});
|
|
232
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
233
|
+
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
if (e instanceof ApiError && e.status === 422) {
|
|
236
|
+
return textResult(JSON.stringify(e.body, null, 2), true);
|
|
237
|
+
}
|
|
238
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
239
|
+
return textResult(`Submit failed: ${msg}`, true);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
// Slash path in Cursor: /techprufer/build-profile
|
|
243
|
+
server.prompt('build-profile', 'TechPrufer — build a profile from PDF job or local resume', {
|
|
244
|
+
jobId: z.string().optional().describe('Optional queued profile job id'),
|
|
245
|
+
}, async ({ jobId }) => ({
|
|
246
|
+
messages: [
|
|
247
|
+
{
|
|
248
|
+
role: 'user',
|
|
249
|
+
content: {
|
|
250
|
+
type: 'text',
|
|
251
|
+
text: [
|
|
252
|
+
'Build TechPrufer master profile(s) via MCP.',
|
|
253
|
+
'',
|
|
254
|
+
'Path A — queued PDF jobs from the site:',
|
|
255
|
+
jobId
|
|
256
|
+
? `1. Call get_profile_job with jobId=${jobId}.`
|
|
257
|
+
: '1. Call list_pending_profile_jobs; for each QUEUED/CLAIMED job call get_profile_job.',
|
|
258
|
+
'2. Execute returned messages verbatim; output ONLY CPM profile JSON.',
|
|
259
|
+
'3. Call submit_profile_job_result. On correctiveMessage, fix once and resubmit.',
|
|
260
|
+
'',
|
|
261
|
+
'Path B — local resume file (user selected / open in this tool):',
|
|
262
|
+
'1. Read the resume file and extract full text.',
|
|
263
|
+
'2. Call get_profile_build_messages with that resumeText.',
|
|
264
|
+
'3. Execute messages verbatim; output ONLY CPM profile JSON.',
|
|
265
|
+
'4. Call submit_built_profile (optional name from file / identity.name).',
|
|
266
|
+
'',
|
|
267
|
+
'Prefer Path A if pending jobs exist; otherwise Path B when a local resume is available.',
|
|
268
|
+
'Zero hallucination. Complete JSON only. Faithful extraction, not rewriting.',
|
|
269
|
+
].join('\n'),
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
}));
|
|
127
274
|
return server;
|
|
128
275
|
}
|
|
129
276
|
export async function startStdioServer() {
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "techprufer-mcp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "TechPrufer
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "TechPrufer MCP — tailor resumes and build profiles from your AI tool",
|
|
5
|
+
"homepage": "https://techprufer.com",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"bin": {
|
|
7
8
|
"techprufer-mcp": "dist/cli.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"dist",
|
|
12
|
+
"assets",
|
|
11
13
|
"README.md"
|
|
12
14
|
],
|
|
13
15
|
"engines": {
|