@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 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
+ }