techprufer-mcp 0.1.0

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 ADDED
@@ -0,0 +1,40 @@
1
+ # techprufer-mcp
2
+
3
+ Local Agent (MCP) for [TechPrufer](https://techprufer.com) — tailor resumes from your AI tool using your own subscription. No Gemini API key required.
4
+
5
+ ## Install (any MCP host)
6
+
7
+ ```json
8
+ {
9
+ "mcpServers": {
10
+ "techprufer": {
11
+ "command": "npx",
12
+ "args": ["-y", "techprufer-mcp"]
13
+ }
14
+ }
15
+ }
16
+ ```
17
+
18
+ ## Login (once)
19
+
20
+ ```bash
21
+ npx techprufer-mcp login
22
+ ```
23
+
24
+ Opens TechPrufer in your browser. Approve the device code. Credentials are stored at `~/.techprufer/credentials.json`.
25
+
26
+ ## Use
27
+
28
+ 1. On techprufer.com, start a tailor with **Local Agent (MCP)**.
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.
31
+
32
+ ## Tools
33
+
34
+ - `list_pending_tailor_jobs`
35
+ - `get_tailor_job` — claim + exact prompt payload
36
+ - `submit_tailor_result` — validate + store (retries with corrective feedback)
37
+
38
+ ## Env
39
+
40
+ - `TECHPRUFER_API_URL` — override API base (default `https://techprufer.com`)
package/dist/api.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export declare class ApiError extends Error {
2
+ status: number;
3
+ body: unknown;
4
+ constructor(status: number, body: unknown, message?: string);
5
+ }
6
+ export declare function apiFetch<T>(path: string, init?: RequestInit & {
7
+ token?: string | null;
8
+ }): Promise<T>;
package/dist/api.js ADDED
@@ -0,0 +1,40 @@
1
+ import { getApiUrl, loadCredentials } from './config.js';
2
+ export class ApiError extends Error {
3
+ status;
4
+ body;
5
+ constructor(status, body, message) {
6
+ super(message || `API ${status}`);
7
+ this.status = status;
8
+ this.body = body;
9
+ }
10
+ }
11
+ export async function apiFetch(path, init = {}) {
12
+ const creds = loadCredentials();
13
+ const token = init.token === undefined ? creds?.token : init.token;
14
+ const base = creds?.apiUrl || getApiUrl();
15
+ const headers = new Headers(init.headers);
16
+ headers.set('Accept', 'application/json');
17
+ if (init.body && !headers.has('Content-Type')) {
18
+ headers.set('Content-Type', 'application/json');
19
+ }
20
+ if (token)
21
+ headers.set('Authorization', `Bearer ${token}`);
22
+ const res = await fetch(`${base}${path}`, { ...init, headers });
23
+ const text = await res.text();
24
+ let body = null;
25
+ if (text) {
26
+ try {
27
+ body = JSON.parse(text);
28
+ }
29
+ catch {
30
+ body = text;
31
+ }
32
+ }
33
+ if (!res.ok) {
34
+ const msg = body && typeof body === 'object' && body !== null && 'error' in body
35
+ ? String(body.error)
36
+ : `HTTP ${res.status}`;
37
+ throw new ApiError(res.status, body, msg);
38
+ }
39
+ return body;
40
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ import { runLogin, startStdioServer } from './server.js';
3
+ import { loadCredentials, credentialsFilePath, getApiUrl } from './config.js';
4
+ async function main() {
5
+ const args = process.argv.slice(2);
6
+ const cmd = args[0];
7
+ if (cmd === 'login') {
8
+ const nameIdx = args.indexOf('--name');
9
+ const deviceName = nameIdx >= 0 ? args[nameIdx + 1] : undefined;
10
+ await runLogin(deviceName);
11
+ return;
12
+ }
13
+ if (cmd === 'status' || cmd === 'whoami') {
14
+ const creds = loadCredentials();
15
+ if (!creds) {
16
+ console.error(`Not logged in. Credentials expected at ${credentialsFilePath()}`);
17
+ console.error('Run: npx techprufer-mcp login');
18
+ process.exit(1);
19
+ }
20
+ console.error(`API: ${creds.apiUrl || getApiUrl()}`);
21
+ console.error(`Token hint: …${creds.token.slice(-4)}`);
22
+ console.error(`Saved: ${creds.savedAt}`);
23
+ return;
24
+ }
25
+ if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
26
+ console.error(`techprufer-mcp — TechPrufer Local Agent (MCP)
27
+
28
+ Usage:
29
+ npx techprufer-mcp Start stdio MCP server (for AI tools)
30
+ npx techprufer-mcp login Device-code login (opens browser)
31
+ npx techprufer-mcp status Show saved credentials
32
+
33
+ Env:
34
+ TECHPRUFER_API_URL Override API base (default https://techprufer.com)
35
+ `);
36
+ return;
37
+ }
38
+ // Default: MCP stdio server
39
+ await startStdioServer();
40
+ }
41
+ main().catch((err) => {
42
+ console.error(err instanceof Error ? err.message : err);
43
+ process.exit(1);
44
+ });
@@ -0,0 +1,11 @@
1
+ export declare const DEFAULT_API_URL = "https://techprufer.com";
2
+ export interface Credentials {
3
+ token: string;
4
+ apiUrl: string;
5
+ deviceId?: string;
6
+ savedAt: string;
7
+ }
8
+ export declare function getApiUrl(): string;
9
+ export declare function loadCredentials(): Credentials | null;
10
+ export declare function saveCredentials(creds: Omit<Credentials, 'savedAt'>): void;
11
+ export declare function credentialsFilePath(): string;
package/dist/config.js ADDED
@@ -0,0 +1,43 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'fs';
4
+ export const DEFAULT_API_URL = 'https://techprufer.com';
5
+ function configDir() {
6
+ return join(homedir(), '.techprufer');
7
+ }
8
+ function credentialsPath() {
9
+ return join(configDir(), 'credentials.json');
10
+ }
11
+ export function getApiUrl() {
12
+ return (process.env.TECHPRUFER_API_URL || DEFAULT_API_URL).replace(/\/$/, '');
13
+ }
14
+ export function loadCredentials() {
15
+ const path = credentialsPath();
16
+ if (!existsSync(path))
17
+ return null;
18
+ try {
19
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
20
+ if (!raw?.token)
21
+ return null;
22
+ return raw;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ export function saveCredentials(creds) {
29
+ const dir = configDir();
30
+ if (!existsSync(dir))
31
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
32
+ const path = credentialsPath();
33
+ writeFileSync(path, JSON.stringify({ ...creds, savedAt: new Date().toISOString() }, null, 2), 'utf8');
34
+ try {
35
+ chmodSync(path, 0o600);
36
+ }
37
+ catch {
38
+ // Windows may ignore chmod — fine.
39
+ }
40
+ }
41
+ export function credentialsFilePath() {
42
+ return credentialsPath();
43
+ }
@@ -0,0 +1 @@
1
+ export declare function runLogin(deviceName?: string): Promise<void>;
package/dist/login.js ADDED
@@ -0,0 +1,59 @@
1
+ import { spawn } from 'child_process';
2
+ import { platform } from 'os';
3
+ import { apiFetch } from './api.js';
4
+ import { getApiUrl, saveCredentials } from './config.js';
5
+ function openBrowser(url) {
6
+ const cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open';
7
+ try {
8
+ const child = spawn(cmd, [url], { detached: true, stdio: 'ignore' });
9
+ child.unref();
10
+ }
11
+ catch {
12
+ // User can open manually.
13
+ }
14
+ }
15
+ function sleep(ms) {
16
+ return new Promise((r) => setTimeout(r, ms));
17
+ }
18
+ export async function runLogin(deviceName) {
19
+ const apiUrl = getApiUrl();
20
+ console.error(`TechPrufer Local Agent — logging in against ${apiUrl}`);
21
+ const start = await apiFetch('/api/agent/device/start', {
22
+ method: 'POST',
23
+ token: null,
24
+ body: JSON.stringify({ deviceName: deviceName || undefined }),
25
+ });
26
+ console.error('');
27
+ console.error(` Code: ${start.userCode}`);
28
+ console.error(` Open: ${start.verifyUrl}`);
29
+ console.error('');
30
+ console.error('Waiting for approval in the browser…');
31
+ openBrowser(start.verifyUrl);
32
+ const intervalMs = Math.max(3, start.interval || 5) * 1000;
33
+ const deadline = Date.now() + start.expiresIn * 1000;
34
+ while (Date.now() < deadline) {
35
+ await sleep(intervalMs);
36
+ const poll = await apiFetch('/api/agent/device/poll', {
37
+ method: 'POST',
38
+ token: null,
39
+ body: JSON.stringify({ deviceCode: start.deviceCode }),
40
+ });
41
+ if (poll.status === 'pending')
42
+ continue;
43
+ if (poll.status === 'expired') {
44
+ console.error('Code expired. Run login again.');
45
+ process.exit(1);
46
+ }
47
+ if (poll.status === 'ready' && poll.token) {
48
+ saveCredentials({
49
+ token: poll.token,
50
+ apiUrl,
51
+ deviceId: poll.deviceId,
52
+ });
53
+ console.error('Logged in. You can close this window and use /tailor in your AI tool.');
54
+ return;
55
+ }
56
+ }
57
+ console.error('Timed out waiting for approval.');
58
+ process.exit(1);
59
+ }
@@ -0,0 +1,6 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { runLogin } from './login.js';
3
+ export declare function createServer(): McpServer;
4
+ export declare function startStdioServer(): Promise<void>;
5
+ /** CLI entry when invoked as `npx techprufer-mcp login`. */
6
+ export { runLogin };
package/dist/server.js ADDED
@@ -0,0 +1,135 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+ import { apiFetch, ApiError } from './api.js';
5
+ import { loadCredentials, credentialsFilePath } from './config.js';
6
+ import { runLogin } from './login.js';
7
+ function textResult(text, isError = false) {
8
+ return { content: [{ type: 'text', text }], isError };
9
+ }
10
+ function requireAuthMessage() {
11
+ return (`Not logged in. Run: npx techprufer-mcp login\n` +
12
+ `Credentials file: ${credentialsFilePath()}\n` +
13
+ `Then retry this tool.`);
14
+ }
15
+ async function ensureToken() {
16
+ const creds = loadCredentials();
17
+ return creds?.token ?? null;
18
+ }
19
+ export function createServer() {
20
+ const server = new McpServer({
21
+ name: 'techprufer',
22
+ version: '0.1.0',
23
+ });
24
+ server.tool('list_pending_tailor_jobs', 'List TechPrufer tailor jobs queued for Local Agent (MCP). Returns id, company, role, status.', {}, async () => {
25
+ const token = await ensureToken();
26
+ if (!token)
27
+ return textResult(requireAuthMessage(), true);
28
+ try {
29
+ const data = await apiFetch('/api/agent/jobs');
30
+ return textResult(JSON.stringify(data.jobs ?? [], null, 2));
31
+ }
32
+ catch (e) {
33
+ const msg = e instanceof Error ? e.message : String(e);
34
+ return textResult(`Failed to list jobs: ${msg}`, true);
35
+ }
36
+ });
37
+ server.tool('get_tailor_job', 'Claim a pending TechPrufer tailor job and return the EXACT prompt messages (same as the Gemini path). ' +
38
+ 'Execute the messages verbatim with your own model and output ONLY the tailored JSON object ' +
39
+ '(keys: experience, projects, skills, meta, linkedinMessage). Then call submit_tailor_result.', {
40
+ jobId: z
41
+ .string()
42
+ .optional()
43
+ .describe('Job id from list_pending_tailor_jobs. If omitted, uses the oldest QUEUED job.'),
44
+ }, async ({ jobId }) => {
45
+ const token = await ensureToken();
46
+ if (!token)
47
+ return textResult(requireAuthMessage(), true);
48
+ try {
49
+ let id = jobId;
50
+ if (!id) {
51
+ const list = await apiFetch('/api/agent/jobs');
52
+ const first = list.jobs.find((j) => j.status === 'QUEUED') ??
53
+ list.jobs.find((j) => j.status === 'CLAIMED');
54
+ if (!first)
55
+ return textResult('No pending tailor jobs.');
56
+ id = first.id;
57
+ }
58
+ const payload = await apiFetch(`/api/agent/jobs/${encodeURIComponent(id)}/payload`);
59
+ return textResult(JSON.stringify(payload, null, 2));
60
+ }
61
+ catch (e) {
62
+ const msg = e instanceof Error ? e.message : String(e);
63
+ return textResult(`Failed to get job: ${msg}`, true);
64
+ }
65
+ });
66
+ server.tool('submit_tailor_result', 'Submit the tailored Profile JSON for a job. Server validates ALL attributes (schema, meta, layout fill). ' +
67
+ 'On 422, read correctiveMessage, fix the JSON, and resubmit (max 2 attempts).', {
68
+ jobId: z.string().describe('Job id'),
69
+ profileJson: z
70
+ .union([z.string(), z.record(z.unknown())])
71
+ .describe('Tailored resume JSON object (or stringified JSON)'),
72
+ model: z
73
+ .string()
74
+ .optional()
75
+ .describe('Model name your agent used (for session history)'),
76
+ }, async ({ jobId, profileJson, model }) => {
77
+ const token = await ensureToken();
78
+ if (!token)
79
+ return textResult(requireAuthMessage(), true);
80
+ try {
81
+ const parsed = typeof profileJson === 'string' ? JSON.parse(profileJson) : profileJson;
82
+ const result = await apiFetch(`/api/agent/jobs/${encodeURIComponent(jobId)}/result`, {
83
+ method: 'POST',
84
+ body: JSON.stringify({ profileJson: parsed, model }),
85
+ });
86
+ return textResult(JSON.stringify(result, null, 2));
87
+ }
88
+ catch (e) {
89
+ if (e instanceof ApiError && e.status === 422) {
90
+ return textResult(JSON.stringify(e.body, null, 2), true);
91
+ }
92
+ const msg = e instanceof Error ? e.message : String(e);
93
+ return textResult(`Submit failed: ${msg}`, true);
94
+ }
95
+ });
96
+ server.prompt('tailor', 'Process pending TechPrufer Local Agent tailor jobs one at a time.', {
97
+ jobId: z
98
+ .string()
99
+ .optional()
100
+ .describe('Optional specific job id; otherwise process all pending jobs in order'),
101
+ }, async ({ jobId }) => ({
102
+ messages: [
103
+ {
104
+ role: 'user',
105
+ content: {
106
+ type: 'text',
107
+ text: [
108
+ 'Process TechPrufer Local Agent (MCP) tailor jobs.',
109
+ jobId
110
+ ? `Focus on jobId=${jobId}.`
111
+ : 'Fetch pending jobs and process them ONE AT A TIME in order.',
112
+ '',
113
+ 'For each job:',
114
+ '1. Call get_tailor_job (with jobId if provided).',
115
+ '2. Execute the returned messages verbatim with your own model.',
116
+ '3. Produce ONLY the tailored JSON object (experience, projects, skills, meta, linkedinMessage).',
117
+ '4. Call submit_tailor_result with that JSON.',
118
+ '5. If the tool returns a correctiveMessage (validation / layout fill), fix the JSON and resubmit once.',
119
+ '6. Proceed to the next pending job.',
120
+ '',
121
+ 'Do not invent experience or metrics. Zero hallucination. Complete output only.',
122
+ ].join('\n'),
123
+ },
124
+ },
125
+ ],
126
+ }));
127
+ return server;
128
+ }
129
+ export async function startStdioServer() {
130
+ const server = createServer();
131
+ const transport = new StdioServerTransport();
132
+ await server.connect(transport);
133
+ }
134
+ /** CLI entry when invoked as `npx techprufer-mcp login`. */
135
+ export { runLogin };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "techprufer-mcp",
3
+ "version": "0.1.0",
4
+ "description": "TechPrufer Local Agent (MCP) — tailor resumes from your AI tool without a Gemini API key",
5
+ "type": "module",
6
+ "bin": {
7
+ "techprufer-mcp": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "dependencies": {
17
+ "@modelcontextprotocol/sdk": "^1.12.1",
18
+ "zod": "^3.24.2"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^20.0.0",
22
+ "typescript": "^5.2.0"
23
+ },
24
+ "keywords": [
25
+ "mcp",
26
+ "techprufer",
27
+ "resume",
28
+ "tailor"
29
+ ],
30
+ "license": "MIT",
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.json"
33
+ }
34
+ }