@snokam/mcp-cvpartner 2.7.0 → 2.8.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/dist/client.d.ts +37 -0
- package/dist/client.js +44 -0
- package/dist/index.js +6 -129
- package/dist/tools/cvs.d.ts +2 -0
- package/dist/tools/cvs.js +171 -0
- package/dist/tools/masterdata.d.ts +2 -0
- package/dist/tools/masterdata.js +39 -0
- package/dist/tools/users.d.ts +2 -0
- package/dist/tools/users.js +57 -0
- package/package.json +4 -4
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare function cvpartnerFetch<T>(endpoint: string, options?: RequestInit): Promise<T>;
|
|
2
|
+
export declare function ok(data: unknown): {
|
|
3
|
+
content: {
|
|
4
|
+
type: "text";
|
|
5
|
+
text: string;
|
|
6
|
+
}[];
|
|
7
|
+
};
|
|
8
|
+
export interface CVPartnerUser {
|
|
9
|
+
user_id: string;
|
|
10
|
+
email: string;
|
|
11
|
+
name: string;
|
|
12
|
+
title?: {
|
|
13
|
+
no?: string;
|
|
14
|
+
int?: string;
|
|
15
|
+
};
|
|
16
|
+
telephone?: string;
|
|
17
|
+
office_name?: string;
|
|
18
|
+
deactivated: boolean;
|
|
19
|
+
default_cv_id?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface CVSearchResult {
|
|
22
|
+
cv: {
|
|
23
|
+
user_id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
email: string;
|
|
26
|
+
title?: string;
|
|
27
|
+
titles?: {
|
|
28
|
+
no?: string;
|
|
29
|
+
int?: string;
|
|
30
|
+
};
|
|
31
|
+
telephone?: string;
|
|
32
|
+
office_id?: string;
|
|
33
|
+
is_deactivated: boolean;
|
|
34
|
+
};
|
|
35
|
+
highlight?: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function resolveCvId(user_id: string, cv_id?: string): Promise<string>;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const API_KEY = process.env.CVPARTNER_API_KEY;
|
|
2
|
+
function resolveBaseUrl() {
|
|
3
|
+
const explicit = process.env.CV_PARTNER_BASE_URL;
|
|
4
|
+
if (explicit && explicit.trim()) {
|
|
5
|
+
return explicit.trim().replace(/\/+$/, "");
|
|
6
|
+
}
|
|
7
|
+
const org = process.env.CVPARTNER_ORG || "snokam";
|
|
8
|
+
return `https://${org}.cvpartner.com/api`;
|
|
9
|
+
}
|
|
10
|
+
const BASE_URL = resolveBaseUrl();
|
|
11
|
+
export async function cvpartnerFetch(endpoint, options = {}) {
|
|
12
|
+
if (!API_KEY) {
|
|
13
|
+
throw new Error("CVPARTNER_API_KEY environment variable is required");
|
|
14
|
+
}
|
|
15
|
+
const path = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
|
16
|
+
const response = await fetch(`${BASE_URL}${path}`, {
|
|
17
|
+
...options,
|
|
18
|
+
headers: {
|
|
19
|
+
Authorization: `Token token="${API_KEY}"`,
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
...options.headers,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
const error = await response.text();
|
|
26
|
+
throw new Error(`CVPartner API error: ${response.status} - ${error || response.statusText}`);
|
|
27
|
+
}
|
|
28
|
+
const text = await response.text();
|
|
29
|
+
return (text ? JSON.parse(text) : undefined);
|
|
30
|
+
}
|
|
31
|
+
export function ok(data) {
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export async function resolveCvId(user_id, cv_id) {
|
|
37
|
+
if (cv_id)
|
|
38
|
+
return cv_id;
|
|
39
|
+
const user = await cvpartnerFetch(`/v1/users/${user_id}`);
|
|
40
|
+
if (!user.default_cv_id) {
|
|
41
|
+
throw new Error(`User ${user_id} has no default_cv_id; pass cv_id explicitly`);
|
|
42
|
+
}
|
|
43
|
+
return user.default_cv_id;
|
|
44
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,139 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const ORG = process.env.CVPARTNER_ORG || "snokam";
|
|
8
|
-
const BASE_URL = `https://${ORG}.cvpartner.com/api`;
|
|
9
|
-
async function cvpartnerFetch(endpoint, options = {}) {
|
|
10
|
-
if (!API_KEY) {
|
|
11
|
-
throw new Error("CVPARTNER_API_KEY environment variable is required");
|
|
12
|
-
}
|
|
13
|
-
const response = await fetch(`${BASE_URL}${endpoint}`, {
|
|
14
|
-
...options,
|
|
15
|
-
headers: {
|
|
16
|
-
Authorization: `Token token=${API_KEY}`,
|
|
17
|
-
"Content-Type": "application/json",
|
|
18
|
-
...options.headers,
|
|
19
|
-
},
|
|
20
|
-
});
|
|
21
|
-
if (!response.ok) {
|
|
22
|
-
const error = await response.text();
|
|
23
|
-
throw new Error(`CVPartner API error: ${response.status} - ${error || response.statusText}`);
|
|
24
|
-
}
|
|
25
|
-
return response.json();
|
|
26
|
-
}
|
|
4
|
+
import { registerUserTools } from "./tools/users.js";
|
|
5
|
+
import { registerCvTools } from "./tools/cvs.js";
|
|
6
|
+
import { registerMasterdataTools } from "./tools/masterdata.js";
|
|
27
7
|
const server = new McpServer({
|
|
28
8
|
name: "cvpartner-mcp",
|
|
29
9
|
version: "1.0.0",
|
|
30
10
|
});
|
|
31
|
-
|
|
32
|
-
server
|
|
33
|
-
|
|
34
|
-
.boolean()
|
|
35
|
-
.default(false)
|
|
36
|
-
.describe("Include deactivated users"),
|
|
37
|
-
}, async ({ include_deactivated }) => {
|
|
38
|
-
const users = await cvpartnerFetch("/v1/users");
|
|
39
|
-
const filtered = include_deactivated
|
|
40
|
-
? users
|
|
41
|
-
: users.filter((u) => !u.deactivated);
|
|
42
|
-
const simplified = filtered.map((u) => ({
|
|
43
|
-
user_id: u.user_id,
|
|
44
|
-
name: u.name,
|
|
45
|
-
email: u.email,
|
|
46
|
-
title: u.title?.no || u.title?.int || null,
|
|
47
|
-
telephone: u.telephone,
|
|
48
|
-
office: u.office_name,
|
|
49
|
-
cv_id: u.default_cv_id,
|
|
50
|
-
}));
|
|
51
|
-
return {
|
|
52
|
-
content: [{ type: "text", text: JSON.stringify(simplified, null, 2) }],
|
|
53
|
-
};
|
|
54
|
-
});
|
|
55
|
-
// Tool: Search CVs
|
|
56
|
-
server.tool("search_cvs", "Search CVs by name, skills, or experience", {
|
|
57
|
-
query: z.string().describe("Search query (name, skill, technology, etc.)"),
|
|
58
|
-
size: z.number().default(10).describe("Maximum number of results"),
|
|
59
|
-
}, async ({ query, size }) => {
|
|
60
|
-
const response = await cvpartnerFetch("/v4/search", {
|
|
61
|
-
method: "POST",
|
|
62
|
-
body: JSON.stringify({ query, size }),
|
|
63
|
-
});
|
|
64
|
-
const results = response.cvs.map((r) => ({
|
|
65
|
-
user_id: r.cv.user_id,
|
|
66
|
-
name: r.cv.name,
|
|
67
|
-
email: r.cv.email,
|
|
68
|
-
title: r.cv.title || r.cv.titles?.no || r.cv.titles?.int,
|
|
69
|
-
telephone: r.cv.telephone,
|
|
70
|
-
highlight: r.highlight,
|
|
71
|
-
}));
|
|
72
|
-
return {
|
|
73
|
-
content: [
|
|
74
|
-
{
|
|
75
|
-
type: "text",
|
|
76
|
-
text: JSON.stringify({ total: response.total, results }, null, 2),
|
|
77
|
-
},
|
|
78
|
-
],
|
|
79
|
-
};
|
|
80
|
-
});
|
|
81
|
-
// Tool: Get user details
|
|
82
|
-
server.tool("get_user", "Get detailed information about a specific user", {
|
|
83
|
-
user_id: z.string().describe("User ID from CVPartner"),
|
|
84
|
-
}, async ({ user_id }) => {
|
|
85
|
-
const user = await cvpartnerFetch(`/v1/users/${user_id}`);
|
|
86
|
-
return {
|
|
87
|
-
content: [{ type: "text", text: JSON.stringify(user, null, 2) }],
|
|
88
|
-
};
|
|
89
|
-
});
|
|
90
|
-
// Tool: Get CV details
|
|
91
|
-
server.tool("get_cv", "Get full CV content for a user", {
|
|
92
|
-
user_id: z.string().describe("User ID"),
|
|
93
|
-
cv_id: z.string().describe("CV ID (from user's default_cv_id)"),
|
|
94
|
-
language: z
|
|
95
|
-
.enum(["no", "int"])
|
|
96
|
-
.default("no")
|
|
97
|
-
.describe("Language version (no=Norwegian, int=English)"),
|
|
98
|
-
}, async ({ user_id, cv_id, language: _language }) => {
|
|
99
|
-
const cv = await cvpartnerFetch(`/v1/cvs/${user_id}/${cv_id}`);
|
|
100
|
-
return {
|
|
101
|
-
content: [{ type: "text", text: JSON.stringify(cv, null, 2) }],
|
|
102
|
-
};
|
|
103
|
-
});
|
|
104
|
-
// Tool: Search by technology/skill
|
|
105
|
-
server.tool("find_consultants_by_skill", "Find consultants with specific skills or technologies", {
|
|
106
|
-
skill: z
|
|
107
|
-
.string()
|
|
108
|
-
.describe("Skill or technology to search for (e.g., React, Python, Azure)"),
|
|
109
|
-
limit: z.number().default(10).describe("Maximum number of results"),
|
|
110
|
-
}, async ({ skill, limit }) => {
|
|
111
|
-
const response = await cvpartnerFetch("/v4/search", {
|
|
112
|
-
method: "POST",
|
|
113
|
-
body: JSON.stringify({
|
|
114
|
-
query: skill,
|
|
115
|
-
size: limit,
|
|
116
|
-
filter_fields: ["technologies", "project_experiences"],
|
|
117
|
-
}),
|
|
118
|
-
});
|
|
119
|
-
const results = response.cvs
|
|
120
|
-
.filter((r) => !r.cv.is_deactivated)
|
|
121
|
-
.map((r) => ({
|
|
122
|
-
name: r.cv.name,
|
|
123
|
-
email: r.cv.email,
|
|
124
|
-
title: r.cv.title || r.cv.titles?.no,
|
|
125
|
-
match: r.highlight,
|
|
126
|
-
}));
|
|
127
|
-
return {
|
|
128
|
-
content: [
|
|
129
|
-
{
|
|
130
|
-
type: "text",
|
|
131
|
-
text: JSON.stringify({ skill, total: response.total, consultants: results }, null, 2),
|
|
132
|
-
},
|
|
133
|
-
],
|
|
134
|
-
};
|
|
135
|
-
});
|
|
136
|
-
// Start server
|
|
11
|
+
registerUserTools(server);
|
|
12
|
+
registerCvTools(server);
|
|
13
|
+
registerMasterdataTools(server);
|
|
137
14
|
async function main() {
|
|
138
15
|
const transport = new StdioServerTransport();
|
|
139
16
|
await server.connect(transport);
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { cvpartnerFetch, ok, resolveCvId, } from "../client.js";
|
|
3
|
+
async function fetchCv(user_id, cv_id) {
|
|
4
|
+
const resolved = await resolveCvId(user_id, cv_id);
|
|
5
|
+
return cvpartnerFetch(`/v3/cvs/${user_id}/${resolved}`);
|
|
6
|
+
}
|
|
7
|
+
function extractSection(cv, candidates) {
|
|
8
|
+
for (const key of candidates) {
|
|
9
|
+
if (cv[key] !== undefined) {
|
|
10
|
+
return { section: key, data: cv[key] };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return { available_sections: Object.keys(cv) };
|
|
14
|
+
}
|
|
15
|
+
const SECTION_TOOLS = [
|
|
16
|
+
{
|
|
17
|
+
name: "get_key_qualifications",
|
|
18
|
+
description: "Get a consultant's key qualifications / summary",
|
|
19
|
+
keys: ["key_qualifications"],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: "get_work_experience",
|
|
23
|
+
description: "Get a consultant's employment / work experience",
|
|
24
|
+
keys: ["work_experiences"],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "get_project_experience",
|
|
28
|
+
description: "Get a consultant's project/assignment experience",
|
|
29
|
+
keys: ["project_experiences"],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "get_education",
|
|
33
|
+
description: "Get a consultant's education history",
|
|
34
|
+
keys: ["educations"],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "get_certifications",
|
|
38
|
+
description: "Get a consultant's certifications",
|
|
39
|
+
keys: ["certifications"],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "get_courses",
|
|
43
|
+
description: "Get a consultant's courses",
|
|
44
|
+
keys: ["courses"],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "get_languages",
|
|
48
|
+
description: "Get the languages a consultant speaks",
|
|
49
|
+
keys: ["languages"],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "get_technologies",
|
|
53
|
+
description: "Get a consultant's technologies / skill categories",
|
|
54
|
+
keys: ["technologies", "technology_skills"],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "get_references",
|
|
58
|
+
description: "Get a consultant's references / recommendations",
|
|
59
|
+
keys: ["recommendations", "references"],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "get_presentations",
|
|
63
|
+
description: "Get a consultant's presentations / talks",
|
|
64
|
+
keys: ["presentations"],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "get_blogs",
|
|
68
|
+
description: "Get a consultant's blog posts / articles",
|
|
69
|
+
keys: ["blogs"],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "get_awards",
|
|
73
|
+
description: "Get a consultant's awards / honors",
|
|
74
|
+
keys: ["honors_awards", "awards", "honors"],
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
export function registerCvTools(server) {
|
|
78
|
+
server.tool("get_cv", "Get full CV content for a consultant", {
|
|
79
|
+
user_id: z.string().describe("User ID"),
|
|
80
|
+
cv_id: z
|
|
81
|
+
.string()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe("CV ID; defaults to the user's default_cv_id when omitted"),
|
|
84
|
+
}, async ({ user_id, cv_id }) => {
|
|
85
|
+
const cv = await fetchCv(user_id, cv_id);
|
|
86
|
+
return ok(cv);
|
|
87
|
+
});
|
|
88
|
+
server.tool("list_cv_sections", "List the available section keys present in a consultant's CV", {
|
|
89
|
+
user_id: z.string().describe("User ID"),
|
|
90
|
+
cv_id: z.string().optional().describe("CV ID; defaults to default_cv_id"),
|
|
91
|
+
}, async ({ user_id, cv_id }) => {
|
|
92
|
+
const cv = await fetchCv(user_id, cv_id);
|
|
93
|
+
return ok({ available_sections: Object.keys(cv) });
|
|
94
|
+
});
|
|
95
|
+
server.tool("get_cv_section", "Get a single named section from a consultant's CV", {
|
|
96
|
+
user_id: z.string().describe("User ID"),
|
|
97
|
+
section: z
|
|
98
|
+
.string()
|
|
99
|
+
.describe("Section key, e.g. project_experiences, work_experiences, technologies, languages"),
|
|
100
|
+
cv_id: z.string().optional().describe("CV ID; defaults to default_cv_id"),
|
|
101
|
+
}, async ({ user_id, section, cv_id }) => {
|
|
102
|
+
const cv = await fetchCv(user_id, cv_id);
|
|
103
|
+
return ok(extractSection(cv, [section]));
|
|
104
|
+
});
|
|
105
|
+
for (const tool of SECTION_TOOLS) {
|
|
106
|
+
server.tool(tool.name, tool.description, {
|
|
107
|
+
user_id: z.string().describe("User ID"),
|
|
108
|
+
cv_id: z
|
|
109
|
+
.string()
|
|
110
|
+
.optional()
|
|
111
|
+
.describe("CV ID; defaults to default_cv_id"),
|
|
112
|
+
}, async ({ user_id, cv_id }) => {
|
|
113
|
+
const cv = await fetchCv(user_id, cv_id);
|
|
114
|
+
return ok(extractSection(cv, tool.keys));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
server.tool("search_cvs", "Search CVs by name, skills, or experience (free text)", {
|
|
118
|
+
query: z
|
|
119
|
+
.string()
|
|
120
|
+
.describe("Search query (name, skill, technology, etc.)"),
|
|
121
|
+
size: z.number().default(10).describe("Maximum number of results"),
|
|
122
|
+
offset: z.number().default(0).describe("Pagination offset"),
|
|
123
|
+
office_ids: z
|
|
124
|
+
.array(z.string())
|
|
125
|
+
.optional()
|
|
126
|
+
.describe("Restrict to specific office IDs"),
|
|
127
|
+
}, async ({ query, size, offset, office_ids }) => {
|
|
128
|
+
const body = { query, size, offset };
|
|
129
|
+
if (office_ids)
|
|
130
|
+
body.office_ids = office_ids;
|
|
131
|
+
const response = await cvpartnerFetch("/v4/search", {
|
|
132
|
+
method: "POST",
|
|
133
|
+
body: JSON.stringify(body),
|
|
134
|
+
});
|
|
135
|
+
const results = response.cvs.map((r) => ({
|
|
136
|
+
user_id: r.cv.user_id,
|
|
137
|
+
cv_id: r.cv.id,
|
|
138
|
+
name: r.cv.name,
|
|
139
|
+
email: r.cv.email,
|
|
140
|
+
title: r.cv.title || r.cv.titles?.no || r.cv.titles?.int,
|
|
141
|
+
telephone: r.cv.telephone,
|
|
142
|
+
highlight: r.highlight,
|
|
143
|
+
}));
|
|
144
|
+
return ok({ total: response.total, results });
|
|
145
|
+
});
|
|
146
|
+
server.tool("find_consultants_by_skill", "Find consultants with specific skills or technologies", {
|
|
147
|
+
skill: z
|
|
148
|
+
.string()
|
|
149
|
+
.describe("Skill or technology to search for (e.g. React, Python, Azure)"),
|
|
150
|
+
limit: z.number().default(10).describe("Maximum number of results"),
|
|
151
|
+
}, async ({ skill, limit }) => {
|
|
152
|
+
const response = await cvpartnerFetch("/v4/search", {
|
|
153
|
+
method: "POST",
|
|
154
|
+
body: JSON.stringify({
|
|
155
|
+
query: skill,
|
|
156
|
+
size: limit,
|
|
157
|
+
filter_fields: ["technologies", "project_experiences"],
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
160
|
+
const results = response.cvs
|
|
161
|
+
.filter((r) => !r.cv.is_deactivated)
|
|
162
|
+
.map((r) => ({
|
|
163
|
+
user_id: r.cv.user_id,
|
|
164
|
+
name: r.cv.name,
|
|
165
|
+
email: r.cv.email,
|
|
166
|
+
title: r.cv.title || r.cv.titles?.no,
|
|
167
|
+
match: r.highlight,
|
|
168
|
+
}));
|
|
169
|
+
return ok({ skill, total: response.total, consultants: results });
|
|
170
|
+
});
|
|
171
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { cvpartnerFetch, ok } from "../client.js";
|
|
3
|
+
export function registerMasterdataTools(server) {
|
|
4
|
+
server.tool("list_countries", "List countries with their offices and departments", {}, async () => {
|
|
5
|
+
const countries = await cvpartnerFetch("/v1/masterdata/countries");
|
|
6
|
+
return ok(countries);
|
|
7
|
+
});
|
|
8
|
+
server.tool("list_offices", "List all offices flattened across countries", {}, async () => {
|
|
9
|
+
const countries = await cvpartnerFetch("/v1/masterdata/countries");
|
|
10
|
+
const offices = countries.flatMap((c) => (c.offices || []).map((o) => ({
|
|
11
|
+
office_id: o.id,
|
|
12
|
+
office_name: o.name,
|
|
13
|
+
country: c.name,
|
|
14
|
+
country_code: c.code,
|
|
15
|
+
})));
|
|
16
|
+
return ok(offices);
|
|
17
|
+
});
|
|
18
|
+
server.tool("list_departments", "List all departments flattened across offices", {}, async () => {
|
|
19
|
+
const countries = await cvpartnerFetch("/v1/masterdata/countries");
|
|
20
|
+
const departments = countries.flatMap((c) => (c.offices || []).flatMap((o) => (o.departments || []).map((d) => ({
|
|
21
|
+
department_id: d.id,
|
|
22
|
+
department_name: d.name,
|
|
23
|
+
office_id: o.id,
|
|
24
|
+
office_name: o.name,
|
|
25
|
+
country: c.name,
|
|
26
|
+
}))));
|
|
27
|
+
return ok(departments);
|
|
28
|
+
});
|
|
29
|
+
server.tool("list_custom_tag_categories", "List custom tag categories and their tags (masterdata)", {}, async () => {
|
|
30
|
+
const categories = await cvpartnerFetch("/v1/masterdata/custom_tags/custom_tag_category");
|
|
31
|
+
return ok(categories);
|
|
32
|
+
});
|
|
33
|
+
server.tool("search_customers", "Search customers/clients by name", {
|
|
34
|
+
name: z.string().describe("Full or partial customer name"),
|
|
35
|
+
}, async ({ name }) => {
|
|
36
|
+
const results = await cvpartnerFetch(`/v2/customers/search?name=${encodeURIComponent(name)}`);
|
|
37
|
+
return ok(results);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { cvpartnerFetch, ok } from "../client.js";
|
|
3
|
+
export function registerUserTools(server) {
|
|
4
|
+
server.tool("list_users", "List all users/consultants in CVPartner", {
|
|
5
|
+
include_deactivated: z
|
|
6
|
+
.boolean()
|
|
7
|
+
.default(false)
|
|
8
|
+
.describe("Include deactivated users"),
|
|
9
|
+
}, async ({ include_deactivated }) => {
|
|
10
|
+
const users = await cvpartnerFetch("/v1/users");
|
|
11
|
+
const filtered = include_deactivated
|
|
12
|
+
? users
|
|
13
|
+
: users.filter((u) => !u.deactivated);
|
|
14
|
+
const simplified = filtered.map((u) => ({
|
|
15
|
+
user_id: u.user_id,
|
|
16
|
+
name: u.name,
|
|
17
|
+
email: u.email,
|
|
18
|
+
title: u.title?.no || u.title?.int || null,
|
|
19
|
+
telephone: u.telephone,
|
|
20
|
+
office: u.office_name,
|
|
21
|
+
cv_id: u.default_cv_id,
|
|
22
|
+
}));
|
|
23
|
+
return ok(simplified);
|
|
24
|
+
});
|
|
25
|
+
server.tool("get_user", "Get detailed information about a specific user/consultant", {
|
|
26
|
+
user_id: z.string().describe("User ID from CVPartner"),
|
|
27
|
+
}, async ({ user_id }) => {
|
|
28
|
+
const user = await cvpartnerFetch(`/v1/users/${user_id}`);
|
|
29
|
+
return ok(user);
|
|
30
|
+
});
|
|
31
|
+
server.tool("list_users_created_after", "List users created after a given date (incremental sync)", {
|
|
32
|
+
created_after: z
|
|
33
|
+
.string()
|
|
34
|
+
.describe("Date in DD-MM-YYYY format, e.g. 27-09-2021"),
|
|
35
|
+
offset: z.number().default(0).describe("Pagination offset"),
|
|
36
|
+
}, async ({ created_after, offset }) => {
|
|
37
|
+
const users = await cvpartnerFetch(`/v1/users?offset=${offset}&created_after=${encodeURIComponent(created_after)}`);
|
|
38
|
+
return ok(users);
|
|
39
|
+
});
|
|
40
|
+
server.tool("search_users", "Search users by name (quick lookup)", {
|
|
41
|
+
name: z.string().describe("Full or partial name to search for"),
|
|
42
|
+
}, async ({ name }) => {
|
|
43
|
+
const results = await cvpartnerFetch(`/v2/users/search?name=${encodeURIComponent(name)}`);
|
|
44
|
+
return ok(results);
|
|
45
|
+
});
|
|
46
|
+
server.tool("search_users_advanced", "Advanced user search across free text, name and technology skills", {
|
|
47
|
+
query: z.string().describe("Free-text query (name, skill, keyword)"),
|
|
48
|
+
size: z.number().default(20).describe("Maximum number of results"),
|
|
49
|
+
offset: z.number().default(0).describe("Pagination offset"),
|
|
50
|
+
}, async ({ query, size, offset }) => {
|
|
51
|
+
const results = await cvpartnerFetch("/v4/users/search", {
|
|
52
|
+
method: "POST",
|
|
53
|
+
body: JSON.stringify({ query, size, offset }),
|
|
54
|
+
});
|
|
55
|
+
return ok(results);
|
|
56
|
+
});
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snokam/mcp-cvpartner",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "MCP server for CVPartner CV management with Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
17
|
-
"zod": "^4.3
|
|
17
|
+
"zod": "^4.4.3"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@types/node": "^
|
|
21
|
-
"tsx": "^4.
|
|
20
|
+
"@types/node": "^26.1.1",
|
|
21
|
+
"tsx": "^4.23.1",
|
|
22
22
|
"typescript": "^6.0.2"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|