@cben0ist/ascend-mcp-server 1.0.1
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/.env.example +10 -0
- package/README.md +87 -0
- package/package.json +20 -0
- package/src/client.js +34 -0
- package/src/index.js +33 -0
- package/src/tools/analytics.js +28 -0
- package/src/tools/applications.js +49 -0
- package/src/tools/index.js +11 -0
- package/src/tools/resume.js +22 -0
- package/src/tools/stats.js +22 -0
package/.env.example
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Ascend MCP Server Configuration
|
|
2
|
+
# Copy this file to .env and fill in your values
|
|
3
|
+
|
|
4
|
+
# Your Ascend API base URL for MCP endpoints
|
|
5
|
+
# Production: https://ascend.workingensemble.ca/api/mcp
|
|
6
|
+
# Local dev: http://localhost:4000/api/mcp
|
|
7
|
+
ASCEND_API_URL=https://ascend.workingensemble.ca/api/mcp
|
|
8
|
+
|
|
9
|
+
# Your Ascend API key (get from Settings → Claude AI Integration)
|
|
10
|
+
ASCEND_API_KEY=your_64_char_hex_key_here
|
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Ascend MCP Server
|
|
2
|
+
|
|
3
|
+
Connect Claude (Desktop, Code, or agents) to your Ascend job search data.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
Gives Claude read-only access to:
|
|
8
|
+
- **Job applications** — list, filter by status, get full details with AI analysis
|
|
9
|
+
- **Resume** — your base resume in JSON Resume format
|
|
10
|
+
- **Dashboard stats** — totals, interview counts, match scores
|
|
11
|
+
- **Analytics** — trends and period-over-period comparisons
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
### 1. Get your API key
|
|
16
|
+
|
|
17
|
+
1. Open Ascend → Settings → **Claude AI Integration**
|
|
18
|
+
2. Click **Generate Key** and copy the key (shown once)
|
|
19
|
+
|
|
20
|
+
### 2. Install dependencies
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
cd mcp
|
|
24
|
+
npm install
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 3. Configure Claude Desktop
|
|
28
|
+
|
|
29
|
+
Add to your `claude_desktop_config.json`:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"ascend": {
|
|
35
|
+
"command": "node",
|
|
36
|
+
"args": ["/absolute/path/to/ascend/mcp/src/index.js"],
|
|
37
|
+
"env": {
|
|
38
|
+
"ASCEND_API_URL": "https://ascend.workingensemble.ca/api/mcp",
|
|
39
|
+
"ASCEND_API_KEY": "your_key_here"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 4. Configure Claude Code
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
claude mcp add ascend \
|
|
50
|
+
-e ASCEND_API_URL=https://ascend.workingensemble.ca/api/mcp \
|
|
51
|
+
-e ASCEND_API_KEY=your_key_here \
|
|
52
|
+
-- node /absolute/path/to/ascend/mcp/src/index.js
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Local development
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
ASCEND_API_URL=http://localhost:4000/api/mcp \
|
|
59
|
+
ASCEND_API_KEY=your_key_here \
|
|
60
|
+
node mcp/src/index.js
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Test with MCP Inspector
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cd mcp
|
|
67
|
+
ASCEND_API_URL=http://localhost:4000/api/mcp ASCEND_API_KEY=your_key \
|
|
68
|
+
npx @modelcontextprotocol/inspector node src/index.js
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Available Tools
|
|
72
|
+
|
|
73
|
+
| Tool | Description |
|
|
74
|
+
|------|-------------|
|
|
75
|
+
| `list_applications` | List applications with optional status filter |
|
|
76
|
+
| `get_application` | Full details for one application including AI analysis |
|
|
77
|
+
| `get_resume` | Your base resume in JSON Resume format |
|
|
78
|
+
| `get_dashboard_stats` | Total applications, interviews, offers, avg match score |
|
|
79
|
+
| `get_analytics_overview` | Trends and analytics for a date range |
|
|
80
|
+
|
|
81
|
+
## Example prompts
|
|
82
|
+
|
|
83
|
+
- "How many job applications do I have in total?"
|
|
84
|
+
- "Show me applications where I'm currently interviewing"
|
|
85
|
+
- "What's my average job match score?"
|
|
86
|
+
- "Summarize my resume"
|
|
87
|
+
- "How has my application rate changed over the last month?"
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cben0ist/ascend-mcp-server",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "MCP server for Ascend — connect Claude to your job applications, resume, and analytics",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ascend-mcp": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
14
|
+
"axios": "^1.7.0",
|
|
15
|
+
"zod": "^3.22.0"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create an Ascend API client authenticated with the given API key.
|
|
5
|
+
* @param {string} baseURL - e.g. http://localhost:4000/api/mcp
|
|
6
|
+
* @param {string} apiKey - 64-char hex API key
|
|
7
|
+
*/
|
|
8
|
+
export function createClient(baseURL, apiKey) {
|
|
9
|
+
const client = axios.create({
|
|
10
|
+
baseURL,
|
|
11
|
+
headers: {
|
|
12
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
},
|
|
15
|
+
timeout: 30000,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Normalize errors for MCP tools
|
|
19
|
+
client.interceptors.response.use(
|
|
20
|
+
(response) => response,
|
|
21
|
+
(error) => {
|
|
22
|
+
const message = error.response?.data?.error
|
|
23
|
+
|| error.response?.data?.message
|
|
24
|
+
|| error.message
|
|
25
|
+
|| 'Unknown error';
|
|
26
|
+
const status = error.response?.status || 0;
|
|
27
|
+
const normalized = new Error(`Ascend API error (${status}): ${message}`);
|
|
28
|
+
normalized.status = status;
|
|
29
|
+
return Promise.reject(normalized);
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return client;
|
|
34
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
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 { createClient } from './client.js';
|
|
5
|
+
import { registerAllTools } from './tools/index.js';
|
|
6
|
+
|
|
7
|
+
const ASCEND_API_URL = process.env.ASCEND_API_URL;
|
|
8
|
+
const ASCEND_API_KEY = process.env.ASCEND_API_KEY;
|
|
9
|
+
|
|
10
|
+
if (!ASCEND_API_URL) {
|
|
11
|
+
console.error('Error: ASCEND_API_URL environment variable is required');
|
|
12
|
+
console.error('Example: ASCEND_API_URL=https://ascend.workingensemble.ca/api/mcp');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!ASCEND_API_KEY) {
|
|
17
|
+
console.error('Error: ASCEND_API_KEY environment variable is required');
|
|
18
|
+
console.error('Get your API key from Settings → Claude AI Integration in the Ascend app');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const client = createClient(ASCEND_API_URL, ASCEND_API_KEY);
|
|
23
|
+
|
|
24
|
+
const server = new McpServer({
|
|
25
|
+
name: 'ascend',
|
|
26
|
+
version: '1.0.0',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
registerAllTools(server, client);
|
|
30
|
+
|
|
31
|
+
const transport = new StdioServerTransport();
|
|
32
|
+
await server.connect(transport);
|
|
33
|
+
console.error('Ascend MCP server running on stdio');
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export function registerAnalyticsTools(server, client) {
|
|
4
|
+
server.tool(
|
|
5
|
+
'get_analytics_overview',
|
|
6
|
+
'Get analytics overview: application trends, interview rates, offer rates, and period-over-period comparisons.',
|
|
7
|
+
{
|
|
8
|
+
startDate: z.string().optional().describe('Start date ISO8601 (default: 30 days ago)'),
|
|
9
|
+
endDate: z.string().optional().describe('End date ISO8601 (default: today)'),
|
|
10
|
+
},
|
|
11
|
+
async ({ startDate, endDate }) => {
|
|
12
|
+
try {
|
|
13
|
+
const params = {};
|
|
14
|
+
if (startDate) params.startDate = startDate;
|
|
15
|
+
if (endDate) params.endDate = endDate;
|
|
16
|
+
const { data } = await client.get('/analytics', { params });
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
19
|
+
};
|
|
20
|
+
} catch (err) {
|
|
21
|
+
return {
|
|
22
|
+
content: [{ type: 'text', text: err.message }],
|
|
23
|
+
isError: true,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export function registerApplicationTools(server, client) {
|
|
4
|
+
server.tool(
|
|
5
|
+
'list_applications',
|
|
6
|
+
'List job applications from Ascend. Returns a paginated list with status, company, job title, and match score.',
|
|
7
|
+
{
|
|
8
|
+
status: z.string().optional().describe('Filter by status (e.g. applied, interviewing, offer, rejected)'),
|
|
9
|
+
limit: z.number().int().min(1).max(100).optional().default(20).describe('Number of results (default 20, max 100)'),
|
|
10
|
+
offset: z.number().int().min(0).optional().default(0).describe('Pagination offset'),
|
|
11
|
+
},
|
|
12
|
+
async ({ status, limit, offset }) => {
|
|
13
|
+
try {
|
|
14
|
+
const params = { limit, offset };
|
|
15
|
+
if (status) params.status = status;
|
|
16
|
+
const { data } = await client.get('/applications', { params });
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
19
|
+
};
|
|
20
|
+
} catch (err) {
|
|
21
|
+
return {
|
|
22
|
+
content: [{ type: 'text', text: err.message }],
|
|
23
|
+
isError: true,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
server.tool(
|
|
30
|
+
'get_application',
|
|
31
|
+
'Get full details for a specific job application including AI analysis, tailored resume, and metadata.',
|
|
32
|
+
{
|
|
33
|
+
id: z.string().describe('Application ID (UUID)'),
|
|
34
|
+
},
|
|
35
|
+
async ({ id }) => {
|
|
36
|
+
try {
|
|
37
|
+
const { data } = await client.get(`/applications/${encodeURIComponent(id)}`);
|
|
38
|
+
return {
|
|
39
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
40
|
+
};
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: 'text', text: err.message }],
|
|
44
|
+
isError: true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { registerApplicationTools } from './applications.js';
|
|
2
|
+
import { registerResumeTools } from './resume.js';
|
|
3
|
+
import { registerStatsTools } from './stats.js';
|
|
4
|
+
import { registerAnalyticsTools } from './analytics.js';
|
|
5
|
+
|
|
6
|
+
export function registerAllTools(server, client) {
|
|
7
|
+
registerApplicationTools(server, client);
|
|
8
|
+
registerResumeTools(server, client);
|
|
9
|
+
registerStatsTools(server, client);
|
|
10
|
+
registerAnalyticsTools(server, client);
|
|
11
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export function registerResumeTools(server, client) {
|
|
4
|
+
server.tool(
|
|
5
|
+
'get_resume',
|
|
6
|
+
'Get the user\'s base resume in JSON Resume format. Includes work history, education, skills, projects, and contact info.',
|
|
7
|
+
{},
|
|
8
|
+
async () => {
|
|
9
|
+
try {
|
|
10
|
+
const { data } = await client.get('/resume');
|
|
11
|
+
return {
|
|
12
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
13
|
+
};
|
|
14
|
+
} catch (err) {
|
|
15
|
+
return {
|
|
16
|
+
content: [{ type: 'text', text: err.message }],
|
|
17
|
+
isError: true,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export function registerStatsTools(server, client) {
|
|
4
|
+
server.tool(
|
|
5
|
+
'get_dashboard_stats',
|
|
6
|
+
'Get dashboard statistics: total applications, active applications, interviews, offers, and average AI match score.',
|
|
7
|
+
{},
|
|
8
|
+
async () => {
|
|
9
|
+
try {
|
|
10
|
+
const { data } = await client.get('/stats');
|
|
11
|
+
return {
|
|
12
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
13
|
+
};
|
|
14
|
+
} catch (err) {
|
|
15
|
+
return {
|
|
16
|
+
content: [{ type: 'text', text: err.message }],
|
|
17
|
+
isError: true,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
}
|