@snokam/mcp-cvpartner 1.0.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 +59 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +145 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# @snokam/mcp-cvpartner
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for CVPartner CV management. Enables AI agents to search and retrieve consultant CVs.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔍 **Search CVs** by name, skills, or experience
|
|
8
|
+
- 👥 **List consultants** with their profiles
|
|
9
|
+
- 📄 **Get full CV content** in Norwegian or English
|
|
10
|
+
- 🛠️ **Find consultants by skill** (React, Python, Azure, etc.)
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
cd mcp/@snokam/cvpartner
|
|
16
|
+
pnpm install
|
|
17
|
+
pnpm build
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Configuration
|
|
21
|
+
|
|
22
|
+
```env
|
|
23
|
+
CVPARTNER_API_KEY=your_api_key
|
|
24
|
+
CVPARTNER_ORG=snokam # Optional, defaults to "snokam"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage with mcporter
|
|
28
|
+
|
|
29
|
+
Add to your mcporter config:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"cvpartner": {
|
|
35
|
+
"command": "node",
|
|
36
|
+
"args": ["/path/to/mcp/@snokam/cvpartner/dist/index.js"],
|
|
37
|
+
"env": {
|
|
38
|
+
"CVPARTNER_ORG": "snokam"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
API key should be set in environment or fetched from 1Password.
|
|
46
|
+
|
|
47
|
+
## Available Tools
|
|
48
|
+
|
|
49
|
+
| Tool | Description |
|
|
50
|
+
| --------------------------- | ------------------------------------- |
|
|
51
|
+
| `list_users` | List all consultants |
|
|
52
|
+
| `search_cvs` | Search CVs by any text |
|
|
53
|
+
| `get_user` | Get user profile details |
|
|
54
|
+
| `get_cv` | Get full CV content |
|
|
55
|
+
| `find_consultants_by_skill` | Find consultants with specific skills |
|
|
56
|
+
|
|
57
|
+
## API Documentation
|
|
58
|
+
|
|
59
|
+
CVPartner API: https://docs.cvpartner.com/
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
// Environment variables
|
|
6
|
+
const API_KEY = process.env.CVPARTNER_API_KEY;
|
|
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
|
+
}
|
|
27
|
+
const server = new McpServer({
|
|
28
|
+
name: "cvpartner-mcp",
|
|
29
|
+
version: "1.0.0",
|
|
30
|
+
});
|
|
31
|
+
// Tool: List all users/consultants
|
|
32
|
+
server.tool("list_users", "List all users/consultants in CVPartner", {
|
|
33
|
+
include_deactivated: z
|
|
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 }) => {
|
|
99
|
+
const cv = await cvpartnerFetch(`/v4/cvs/${user_id}/${cv_id}/${language}`);
|
|
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
|
|
137
|
+
async function main() {
|
|
138
|
+
const transport = new StdioServerTransport();
|
|
139
|
+
await server.connect(transport);
|
|
140
|
+
console.error("[cvpartner-mcp] Server running on stdio");
|
|
141
|
+
}
|
|
142
|
+
main().catch((error) => {
|
|
143
|
+
console.error("[cvpartner-mcp] Fatal error:", error);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@snokam/mcp-cvpartner",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for CVPartner CV management",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"snokam-cvpartner-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsx src/index.ts",
|
|
13
|
+
"start": "node dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
17
|
+
"zod": "^3.25.76"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.0.0",
|
|
21
|
+
"tsx": "^4.19.0",
|
|
22
|
+
"typescript": "^5.7.0"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
}
|
|
30
|
+
}
|