@hasna/microservices 0.0.4 → 0.0.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/microservices/microservice-ads/src/cli/index.ts +198 -0
- package/microservices/microservice-ads/src/db/campaigns.ts +304 -0
- package/microservices/microservice-ads/src/mcp/index.ts +160 -0
- package/microservices/microservice-contracts/src/cli/index.ts +410 -23
- package/microservices/microservice-contracts/src/db/contracts.ts +430 -1
- package/microservices/microservice-contracts/src/db/migrations.ts +83 -0
- package/microservices/microservice-contracts/src/mcp/index.ts +312 -3
- package/microservices/microservice-domains/src/cli/index.ts +253 -0
- package/microservices/microservice-domains/src/db/domains.ts +613 -0
- package/microservices/microservice-domains/src/index.ts +21 -0
- package/microservices/microservice-domains/src/mcp/index.ts +168 -0
- package/microservices/microservice-hiring/src/cli/index.ts +318 -8
- package/microservices/microservice-hiring/src/db/hiring.ts +503 -0
- package/microservices/microservice-hiring/src/db/migrations.ts +21 -0
- package/microservices/microservice-hiring/src/index.ts +29 -0
- package/microservices/microservice-hiring/src/lib/scoring.ts +206 -0
- package/microservices/microservice-hiring/src/mcp/index.ts +245 -0
- package/microservices/microservice-payments/src/cli/index.ts +255 -3
- package/microservices/microservice-payments/src/db/migrations.ts +18 -0
- package/microservices/microservice-payments/src/db/payments.ts +552 -0
- package/microservices/microservice-payments/src/mcp/index.ts +223 -0
- package/microservices/microservice-payroll/src/cli/index.ts +269 -0
- package/microservices/microservice-payroll/src/db/migrations.ts +26 -0
- package/microservices/microservice-payroll/src/db/payroll.ts +636 -0
- package/microservices/microservice-payroll/src/mcp/index.ts +246 -0
- package/microservices/microservice-shipping/src/cli/index.ts +211 -3
- package/microservices/microservice-shipping/src/db/migrations.ts +8 -0
- package/microservices/microservice-shipping/src/db/shipping.ts +453 -3
- package/microservices/microservice-shipping/src/mcp/index.ts +149 -1
- package/microservices/microservice-social/src/cli/index.ts +244 -2
- package/microservices/microservice-social/src/db/migrations.ts +33 -0
- package/microservices/microservice-social/src/db/social.ts +378 -4
- package/microservices/microservice-social/src/mcp/index.ts +221 -1
- package/microservices/microservice-subscriptions/src/cli/index.ts +315 -0
- package/microservices/microservice-subscriptions/src/db/migrations.ts +68 -0
- package/microservices/microservice-subscriptions/src/db/subscriptions.ts +567 -3
- package/microservices/microservice-subscriptions/src/mcp/index.ts +267 -1
- package/package.json +1 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI resume scoring — uses OpenAI or Anthropic to evaluate applicant fit
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getApplicant, getJob, updateApplicant } from "../db/hiring.js";
|
|
6
|
+
import type { Applicant, Job } from "../db/hiring.js";
|
|
7
|
+
|
|
8
|
+
export interface ScoreResult {
|
|
9
|
+
match_pct: number;
|
|
10
|
+
strengths: string[];
|
|
11
|
+
gaps: string[];
|
|
12
|
+
recommendation: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RankEntry {
|
|
16
|
+
applicant: Applicant;
|
|
17
|
+
score: ScoreResult;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ---- Provider abstraction ----
|
|
21
|
+
|
|
22
|
+
async function callAI(prompt: string): Promise<string> {
|
|
23
|
+
// Try Anthropic first, then OpenAI
|
|
24
|
+
const anthropicKey = process.env["ANTHROPIC_API_KEY"];
|
|
25
|
+
const openaiKey = process.env["OPENAI_API_KEY"];
|
|
26
|
+
|
|
27
|
+
if (anthropicKey) {
|
|
28
|
+
return callAnthropic(anthropicKey, prompt);
|
|
29
|
+
}
|
|
30
|
+
if (openaiKey) {
|
|
31
|
+
return callOpenAI(openaiKey, prompt);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
throw new Error(
|
|
35
|
+
"No AI API key found. Set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable."
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function callAnthropic(apiKey: string, prompt: string): Promise<string> {
|
|
40
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
"x-api-key": apiKey,
|
|
45
|
+
"anthropic-version": "2023-06-01",
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
model: "claude-sonnet-4-20250514",
|
|
49
|
+
max_tokens: 1024,
|
|
50
|
+
messages: [{ role: "user", content: prompt }],
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
const text = await response.text();
|
|
56
|
+
throw new Error(`Anthropic API error ${response.status}: ${text}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const data = (await response.json()) as {
|
|
60
|
+
content: Array<{ type: string; text: string }>;
|
|
61
|
+
};
|
|
62
|
+
return data.content[0]?.text || "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function callOpenAI(apiKey: string, prompt: string): Promise<string> {
|
|
66
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: {
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
Authorization: `Bearer ${apiKey}`,
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
model: "gpt-4o-mini",
|
|
74
|
+
messages: [{ role: "user", content: prompt }],
|
|
75
|
+
max_tokens: 1024,
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
const text = await response.text();
|
|
81
|
+
throw new Error(`OpenAI API error ${response.status}: ${text}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const data = (await response.json()) as {
|
|
85
|
+
choices: Array<{ message: { content: string } }>;
|
|
86
|
+
};
|
|
87
|
+
return data.choices[0]?.message?.content || "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---- Scoring ----
|
|
91
|
+
|
|
92
|
+
function buildScoringPrompt(job: Job, applicant: Applicant): string {
|
|
93
|
+
const requirements = job.requirements.length
|
|
94
|
+
? job.requirements.join(", ")
|
|
95
|
+
: "No specific requirements listed";
|
|
96
|
+
|
|
97
|
+
const applicantInfo = [
|
|
98
|
+
`Name: ${applicant.name}`,
|
|
99
|
+
applicant.resume_url ? `Resume: ${applicant.resume_url}` : null,
|
|
100
|
+
applicant.notes ? `Notes/Summary: ${applicant.notes}` : null,
|
|
101
|
+
applicant.source ? `Source: ${applicant.source}` : null,
|
|
102
|
+
applicant.stage ? `Current stage: ${applicant.stage}` : null,
|
|
103
|
+
Object.keys(applicant.metadata).length > 0
|
|
104
|
+
? `Additional info: ${JSON.stringify(applicant.metadata)}`
|
|
105
|
+
: null,
|
|
106
|
+
]
|
|
107
|
+
.filter(Boolean)
|
|
108
|
+
.join("\n");
|
|
109
|
+
|
|
110
|
+
return `You are an expert hiring evaluator. Analyze this applicant against the job requirements and return a JSON assessment.
|
|
111
|
+
|
|
112
|
+
JOB:
|
|
113
|
+
Title: ${job.title}
|
|
114
|
+
Department: ${job.department || "N/A"}
|
|
115
|
+
Description: ${job.description || "N/A"}
|
|
116
|
+
Requirements: ${requirements}
|
|
117
|
+
Salary Range: ${job.salary_range || "N/A"}
|
|
118
|
+
|
|
119
|
+
APPLICANT:
|
|
120
|
+
${applicantInfo}
|
|
121
|
+
|
|
122
|
+
Return ONLY a valid JSON object (no markdown, no code fences) with this exact structure:
|
|
123
|
+
{
|
|
124
|
+
"match_pct": <number 0-100>,
|
|
125
|
+
"strengths": ["strength1", "strength2"],
|
|
126
|
+
"gaps": ["gap1", "gap2"],
|
|
127
|
+
"recommendation": "<hire/strong_hire/no_hire/maybe> — brief explanation"
|
|
128
|
+
}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseScoreResponse(text: string): ScoreResult {
|
|
132
|
+
// Strip markdown code fences if present
|
|
133
|
+
let cleaned = text.trim();
|
|
134
|
+
if (cleaned.startsWith("```")) {
|
|
135
|
+
cleaned = cleaned.replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```\s*$/, "");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const parsed = JSON.parse(cleaned);
|
|
140
|
+
return {
|
|
141
|
+
match_pct: Math.max(0, Math.min(100, Number(parsed.match_pct) || 0)),
|
|
142
|
+
strengths: Array.isArray(parsed.strengths) ? parsed.strengths : [],
|
|
143
|
+
gaps: Array.isArray(parsed.gaps) ? parsed.gaps : [],
|
|
144
|
+
recommendation: String(parsed.recommendation || "Unable to determine"),
|
|
145
|
+
};
|
|
146
|
+
} catch {
|
|
147
|
+
return {
|
|
148
|
+
match_pct: 0,
|
|
149
|
+
strengths: [],
|
|
150
|
+
gaps: [],
|
|
151
|
+
recommendation: `AI response could not be parsed: ${text.slice(0, 200)}`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function scoreApplicant(applicantId: string): Promise<ScoreResult> {
|
|
157
|
+
const applicant = getApplicant(applicantId);
|
|
158
|
+
if (!applicant) throw new Error(`Applicant '${applicantId}' not found`);
|
|
159
|
+
|
|
160
|
+
const job = getJob(applicant.job_id);
|
|
161
|
+
if (!job) throw new Error(`Job '${applicant.job_id}' not found`);
|
|
162
|
+
|
|
163
|
+
const prompt = buildScoringPrompt(job, applicant);
|
|
164
|
+
const response = await callAI(prompt);
|
|
165
|
+
const score = parseScoreResponse(response);
|
|
166
|
+
|
|
167
|
+
// Store in applicant metadata
|
|
168
|
+
const metadata = { ...applicant.metadata, ai_score: score, scored_at: new Date().toISOString() };
|
|
169
|
+
updateApplicant(applicantId, { metadata });
|
|
170
|
+
|
|
171
|
+
return score;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function rankApplicants(jobId: string): Promise<RankEntry[]> {
|
|
175
|
+
const job = getJob(jobId);
|
|
176
|
+
if (!job) throw new Error(`Job '${jobId}' not found`);
|
|
177
|
+
|
|
178
|
+
const applicants = (await import("../db/hiring.js")).listApplicants({ job_id: jobId });
|
|
179
|
+
|
|
180
|
+
const results: RankEntry[] = [];
|
|
181
|
+
|
|
182
|
+
for (const applicant of applicants) {
|
|
183
|
+
// Use cached score if available and recent (less than 24h old)
|
|
184
|
+
const cached = applicant.metadata?.ai_score as ScoreResult | undefined;
|
|
185
|
+
const cachedAt = applicant.metadata?.scored_at as string | undefined;
|
|
186
|
+
const isFresh =
|
|
187
|
+
cachedAt && Date.now() - new Date(cachedAt).getTime() < 24 * 60 * 60 * 1000;
|
|
188
|
+
|
|
189
|
+
if (cached && isFresh) {
|
|
190
|
+
results.push({ applicant, score: cached });
|
|
191
|
+
} else {
|
|
192
|
+
const score = await scoreApplicant(applicant.id);
|
|
193
|
+
// Re-fetch to get updated metadata
|
|
194
|
+
const updated = getApplicant(applicant.id)!;
|
|
195
|
+
results.push({ applicant: updated, score });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Sort by match_pct descending
|
|
200
|
+
results.sort((a, b) => b.score.match_pct - a.score.match_pct);
|
|
201
|
+
|
|
202
|
+
return results;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Exported for testing
|
|
206
|
+
export { buildScoringPrompt, parseScoreResponse, callAI };
|
|
@@ -26,7 +26,18 @@ import {
|
|
|
26
26
|
updateInterview,
|
|
27
27
|
addInterviewFeedback,
|
|
28
28
|
deleteInterview,
|
|
29
|
+
bulkImportApplicants,
|
|
30
|
+
generateOffer,
|
|
31
|
+
getHiringForecast,
|
|
32
|
+
submitStructuredFeedback,
|
|
33
|
+
bulkReject,
|
|
34
|
+
getReferralStats,
|
|
35
|
+
saveJobAsTemplate,
|
|
36
|
+
createJobFromTemplate,
|
|
37
|
+
listJobTemplates,
|
|
38
|
+
deleteJobTemplate,
|
|
29
39
|
} from "../db/hiring.js";
|
|
40
|
+
import { scoreApplicant, rankApplicants } from "../lib/scoring.js";
|
|
30
41
|
|
|
31
42
|
const server = new McpServer({
|
|
32
43
|
name: "microservice-hiring",
|
|
@@ -451,6 +462,240 @@ server.registerTool(
|
|
|
451
462
|
}
|
|
452
463
|
);
|
|
453
464
|
|
|
465
|
+
// --- Bulk Import ---
|
|
466
|
+
|
|
467
|
+
server.registerTool(
|
|
468
|
+
"bulk_import_applicants",
|
|
469
|
+
{
|
|
470
|
+
title: "Bulk Import Applicants",
|
|
471
|
+
description: "Import applicants from CSV data (columns: name,email,phone,job_id,source,resume_url).",
|
|
472
|
+
inputSchema: {
|
|
473
|
+
csv_data: z.string().describe("CSV string with header row"),
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
async ({ csv_data }) => {
|
|
477
|
+
const result = bulkImportApplicants(csv_data);
|
|
478
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
// --- AI Scoring ---
|
|
483
|
+
|
|
484
|
+
server.registerTool(
|
|
485
|
+
"score_applicant",
|
|
486
|
+
{
|
|
487
|
+
title: "Score Applicant",
|
|
488
|
+
description: "AI-score an applicant against job requirements. Returns match percentage, strengths, gaps, and recommendation.",
|
|
489
|
+
inputSchema: { id: z.string() },
|
|
490
|
+
},
|
|
491
|
+
async ({ id }) => {
|
|
492
|
+
try {
|
|
493
|
+
const score = await scoreApplicant(id);
|
|
494
|
+
return { content: [{ type: "text", text: JSON.stringify(score, null, 2) }] };
|
|
495
|
+
} catch (err) {
|
|
496
|
+
return { content: [{ type: "text", text: String(err instanceof Error ? err.message : err) }], isError: true };
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
server.registerTool(
|
|
502
|
+
"rank_applicants",
|
|
503
|
+
{
|
|
504
|
+
title: "Rank Applicants",
|
|
505
|
+
description: "AI-rank all applicants for a job by fit score, sorted best-first.",
|
|
506
|
+
inputSchema: { job_id: z.string() },
|
|
507
|
+
},
|
|
508
|
+
async ({ job_id }) => {
|
|
509
|
+
try {
|
|
510
|
+
const ranked = await rankApplicants(job_id);
|
|
511
|
+
return { content: [{ type: "text", text: JSON.stringify(ranked, null, 2) }] };
|
|
512
|
+
} catch (err) {
|
|
513
|
+
return { content: [{ type: "text", text: String(err instanceof Error ? err.message : err) }], isError: true };
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
// --- Offer Letter ---
|
|
519
|
+
|
|
520
|
+
server.registerTool(
|
|
521
|
+
"generate_offer",
|
|
522
|
+
{
|
|
523
|
+
title: "Generate Offer Letter",
|
|
524
|
+
description: "Generate a Markdown offer letter for an applicant.",
|
|
525
|
+
inputSchema: {
|
|
526
|
+
id: z.string().describe("Applicant ID"),
|
|
527
|
+
salary: z.number().describe("Annual salary"),
|
|
528
|
+
start_date: z.string().describe("Start date (YYYY-MM-DD)"),
|
|
529
|
+
position_title: z.string().optional(),
|
|
530
|
+
department: z.string().optional(),
|
|
531
|
+
benefits: z.string().optional(),
|
|
532
|
+
equity: z.string().optional(),
|
|
533
|
+
signing_bonus: z.number().optional(),
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
async ({ id, salary, start_date, ...rest }) => {
|
|
537
|
+
try {
|
|
538
|
+
const letter = generateOffer(id, { salary, start_date, ...rest });
|
|
539
|
+
return { content: [{ type: "text", text: letter }] };
|
|
540
|
+
} catch (err) {
|
|
541
|
+
return { content: [{ type: "text", text: String(err instanceof Error ? err.message : err) }], isError: true };
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
// --- Pipeline Velocity / Forecast ---
|
|
547
|
+
|
|
548
|
+
server.registerTool(
|
|
549
|
+
"hiring_forecast",
|
|
550
|
+
{
|
|
551
|
+
title: "Hiring Forecast",
|
|
552
|
+
description: "Estimate days-to-fill based on average time between pipeline stages.",
|
|
553
|
+
inputSchema: { job_id: z.string() },
|
|
554
|
+
},
|
|
555
|
+
async ({ job_id }) => {
|
|
556
|
+
try {
|
|
557
|
+
const forecast = getHiringForecast(job_id);
|
|
558
|
+
return { content: [{ type: "text", text: JSON.stringify(forecast, null, 2) }] };
|
|
559
|
+
} catch (err) {
|
|
560
|
+
return { content: [{ type: "text", text: String(err instanceof Error ? err.message : err) }], isError: true };
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
// --- Structured Feedback ---
|
|
566
|
+
|
|
567
|
+
server.registerTool(
|
|
568
|
+
"submit_structured_feedback",
|
|
569
|
+
{
|
|
570
|
+
title: "Submit Structured Interview Feedback",
|
|
571
|
+
description: "Submit scored interview feedback with dimensions (technical, communication, culture_fit, etc.).",
|
|
572
|
+
inputSchema: {
|
|
573
|
+
id: z.string().describe("Interview ID"),
|
|
574
|
+
feedback_text: z.string().optional(),
|
|
575
|
+
technical: z.number().min(1).max(5).optional(),
|
|
576
|
+
communication: z.number().min(1).max(5).optional(),
|
|
577
|
+
culture_fit: z.number().min(1).max(5).optional(),
|
|
578
|
+
problem_solving: z.number().min(1).max(5).optional(),
|
|
579
|
+
leadership: z.number().min(1).max(5).optional(),
|
|
580
|
+
overall: z.number().min(1).max(5).optional(),
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
async ({ id, feedback_text, ...scores }) => {
|
|
584
|
+
const interview = submitStructuredFeedback(id, scores, feedback_text);
|
|
585
|
+
if (!interview) {
|
|
586
|
+
return { content: [{ type: "text", text: `Interview '${id}' not found.` }], isError: true };
|
|
587
|
+
}
|
|
588
|
+
return { content: [{ type: "text", text: JSON.stringify(interview, null, 2) }] };
|
|
589
|
+
}
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
// --- Bulk Rejection ---
|
|
593
|
+
|
|
594
|
+
server.registerTool(
|
|
595
|
+
"bulk_reject",
|
|
596
|
+
{
|
|
597
|
+
title: "Bulk Reject Applicants",
|
|
598
|
+
description: "Bulk reject all applicants for a job matching a specific status.",
|
|
599
|
+
inputSchema: {
|
|
600
|
+
job_id: z.string(),
|
|
601
|
+
status: z.enum(["applied", "screening", "interviewing", "offered"]),
|
|
602
|
+
reason: z.string().optional(),
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
async ({ job_id, status, reason }) => {
|
|
606
|
+
const result = bulkReject(job_id, status, reason);
|
|
607
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
608
|
+
}
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
// --- Referral Stats ---
|
|
612
|
+
|
|
613
|
+
server.registerTool(
|
|
614
|
+
"referral_stats",
|
|
615
|
+
{
|
|
616
|
+
title: "Referral Stats",
|
|
617
|
+
description: "Show conversion rates by applicant source/referral channel.",
|
|
618
|
+
inputSchema: {},
|
|
619
|
+
},
|
|
620
|
+
async () => {
|
|
621
|
+
const stats = getReferralStats();
|
|
622
|
+
return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }] };
|
|
623
|
+
}
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
// --- Job Templates ---
|
|
627
|
+
|
|
628
|
+
server.registerTool(
|
|
629
|
+
"save_job_template",
|
|
630
|
+
{
|
|
631
|
+
title: "Save Job as Template",
|
|
632
|
+
description: "Save an existing job posting as a reusable template.",
|
|
633
|
+
inputSchema: {
|
|
634
|
+
job_id: z.string(),
|
|
635
|
+
name: z.string().describe("Unique template name"),
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
async ({ job_id, name }) => {
|
|
639
|
+
try {
|
|
640
|
+
const template = saveJobAsTemplate(job_id, name);
|
|
641
|
+
return { content: [{ type: "text", text: JSON.stringify(template, null, 2) }] };
|
|
642
|
+
} catch (err) {
|
|
643
|
+
return { content: [{ type: "text", text: String(err instanceof Error ? err.message : err) }], isError: true };
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
server.registerTool(
|
|
649
|
+
"create_job_from_template",
|
|
650
|
+
{
|
|
651
|
+
title: "Create Job from Template",
|
|
652
|
+
description: "Create a new job posting from an existing template.",
|
|
653
|
+
inputSchema: {
|
|
654
|
+
template_name: z.string(),
|
|
655
|
+
title: z.string().optional(),
|
|
656
|
+
department: z.string().optional(),
|
|
657
|
+
location: z.string().optional(),
|
|
658
|
+
salary_range: z.string().optional(),
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
async ({ template_name, ...overrides }) => {
|
|
662
|
+
try {
|
|
663
|
+
const job = createJobFromTemplate(template_name, overrides);
|
|
664
|
+
return { content: [{ type: "text", text: JSON.stringify(job, null, 2) }] };
|
|
665
|
+
} catch (err) {
|
|
666
|
+
return { content: [{ type: "text", text: String(err instanceof Error ? err.message : err) }], isError: true };
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
server.registerTool(
|
|
672
|
+
"list_job_templates",
|
|
673
|
+
{
|
|
674
|
+
title: "List Job Templates",
|
|
675
|
+
description: "List all saved job templates.",
|
|
676
|
+
inputSchema: {},
|
|
677
|
+
},
|
|
678
|
+
async () => {
|
|
679
|
+
const templates = listJobTemplates();
|
|
680
|
+
return {
|
|
681
|
+
content: [{ type: "text", text: JSON.stringify({ templates, count: templates.length }, null, 2) }],
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
server.registerTool(
|
|
687
|
+
"delete_job_template",
|
|
688
|
+
{
|
|
689
|
+
title: "Delete Job Template",
|
|
690
|
+
description: "Delete a job template by ID.",
|
|
691
|
+
inputSchema: { id: z.string() },
|
|
692
|
+
},
|
|
693
|
+
async ({ id }) => {
|
|
694
|
+
const deleted = deleteJobTemplate(id);
|
|
695
|
+
return { content: [{ type: "text", text: JSON.stringify({ id, deleted }) }] };
|
|
696
|
+
}
|
|
697
|
+
);
|
|
698
|
+
|
|
454
699
|
// --- Start ---
|
|
455
700
|
async function main() {
|
|
456
701
|
const transport = new StdioServerTransport();
|