#
tokens: 26289/50000 22/22 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .env.example
├── .gitignore
├── .prettierrc
├── apps
│   └── registry
│       └── scripts
│           └── jobs
│               └── recommendations
│                   └── product-requirements.md
├── Dockerfile
├── index.ts
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│   ├── codebase.ts
│   ├── github.ts
│   ├── openai.ts
│   ├── resume-enhancer.ts
│   ├── schemas.ts
│   ├── tools.ts
│   └── types.ts
├── tests
│   ├── check-openai.ts
│   ├── debug-enhance.ts
│   ├── debug-mock.ts
│   ├── test-direct.js
│   └── test-mcp.js
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | dist
2 | node_modules
3 | .env
4 | .awolf
```

--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------

```
 1 | {
 2 |   "printWidth": 100,
 3 |   "tabWidth": 2,
 4 |   "useTabs": false,
 5 |   "semi": true,
 6 |   "singleQuote": false,
 7 |   "trailingComma": "es5",
 8 |   "bracketSpacing": true,
 9 |   "arrowParens": "always",
10 |   "ignore": ["dist/**/*"]
11 | }
12 | 
```

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
 1 | # Environment variables for jsonresume-mcp
 2 | # Copy this file to .env and fill in your values
 3 | 
 4 | # GitHub personal access token with gist permissions
 5 | GITHUB_TOKEN=your_github_token_here
 6 | 
 7 | # OpenAI API key
 8 | OPENAI_API_KEY=your_openai_api_key_here
 9 | 
10 | # GitHub username
11 | GITHUB_USERNAME=your_github_username_here
12 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # JSON Resume MCP Server
  2 | 
  3 | <div align="center">
  4 | 
  5 | ![JSON Resume Logo](https://jsonresume.org/img/logo.svg)
  6 | 
  7 | [![npm version](https://img.shields.io/npm/v/@jsonresume/mcp.svg)](https://www.npmjs.com/package/@jsonresume/mcp)
  8 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
  9 | [![GitHub Issues](https://img.shields.io/github/issues/jsonresume/mcp.svg)](https://github.com/jsonresume/mcp/issues)
 10 | [![smithery badge](https://smithery.ai/badge/@jsonresume/mcp)](https://smithery.ai/server/@jsonresume/mcp)
 11 | 
 12 | **Automate your resume updates with AI by analyzing your coding projects**
 13 | 
 14 | [Installation](#installation) • [Features](#features) • [Usage](#usage) • [Configuration](#configuration) • [Contributing](#contributing) • [Testing](#testing)
 15 | 
 16 | </div>
 17 | 
 18 | ## What is JSON Resume MCP Server?
 19 | 
 20 | This is a [Model Context Protocol (MCP)](https://modelcontextprotocol.ai) server that enhances AI assistants with the ability to update your [JSON Resume](https://jsonresume.org) by analyzing your coding projects. The MCP server provides tools that allow AI assistants like those in [Windsurf](https://www.windsurf.io/) or [Cursor](https://cursor.sh/) to:
 21 | 
 22 | 1. Check if you have an existing JSON Resume
 23 | 2. Analyze your codebase to understand your technical skills and projects
 24 | 3. Enhance your resume with details about your current project
 25 | 
 26 | With this tool, you can simply ask your AI assistant to "enhance my resume with my current project," and it will automatically analyze your code, extract relevant skills and project details, and update your resume accordingly.
 27 | 
 28 | Video demo: [https://x.com/ajaxdavis/status/1896953226282594381](https://x.com/ajaxdavis/status/1896953226282594381)
 29 | 
 30 | ## Features
 31 | 
 32 | - **Resume Enhancement**: Automatically analyzes your codebase and adds project details to your resume
 33 | - **GitHub Integration**: Fetches and updates your resume stored in GitHub Gists
 34 | - **AI-Powered**: Uses OpenAI to generate professional descriptions of your projects and skills
 35 | - **TypeScript/Zod Validation**: Ensures your resume follows the JSON Resume standard
 36 | - **JSON Resume Ecosystem**: Compatible with the [JSON Resume registry](https://registry.jsonresume.org)
 37 | 
 38 | ## Installation
 39 | 
 40 | ### Prerequisites
 41 | 
 42 | - GitHub account with a personal access token (with gist scope)
 43 | - OpenAI API key
 44 | - Node.js 18+
 45 | - An IDE with MCP support (Windsurf or Cursor)
 46 | 
 47 | ### Installing via Smithery
 48 | 
 49 | To install mcp for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@jsonresume/mcp):
 50 | 
 51 | ```bash
 52 | npx -y @smithery/cli install @jsonresume/mcp --client claude
 53 | ```
 54 | 
 55 | ### Install via NPM
 56 | 
 57 | ```bash
 58 | npm install -g @jsonresume/mcp
 59 | ```
 60 | 
 61 | ### Install in Windsurf or Cursor
 62 | 
 63 | Add the following to your Windsurf or Cursor configuration:
 64 | 
 65 | #### Windsurf
 66 | 
 67 | Open Settings → MCP Servers and add:
 68 | 
 69 | ```json
 70 | {
 71 |   "jsonresume": {
 72 |     "command": "npx",
 73 |     "args": ["-y", "@jsonresume/mcp"],
 74 |     "env": {
 75 |       "GITHUB_TOKEN": "your-github-token",
 76 |       "OPENAI_API_KEY": "your-openai-api-key",
 77 |       "GITHUB_USERNAME": "your-github-username"
 78 |     }
 79 |   }
 80 | }
 81 | ```
 82 | 
 83 | #### Cursor
 84 | 
 85 | Add to your `~/.cursor/mcp_config.json`:
 86 | 
 87 | ```json
 88 | {
 89 |   "mcpServers": {
 90 |     "jsonresume": {
 91 |       "command": "npx",
 92 |       "args": ["-y", "@jsonresume/mcp"],
 93 |       "env": {
 94 |         "GITHUB_TOKEN": "your-github-token",
 95 |         "OPENAI_API_KEY": "your-openai-api-key",
 96 |         "GITHUB_USERNAME": "your-github-username"
 97 |       }
 98 |     }
 99 |   }
100 | }
101 | ```
102 | 
103 | ## Usage
104 | 
105 | Once installed and configured, you can use the following commands with your AI assistant:
106 | 
107 | ### Enhance Your Resume with Current Project
108 | 
109 | Ask your AI assistant:
110 | ```
111 | "Can you enhance my resume with details from my current project?"
112 | ```
113 | 
114 | The assistant will:
115 | 1. Find your existing resume on GitHub (or create a new one if needed)
116 | 2. Analyze your current project's codebase
117 | 3. Generate professional descriptions of your project and skills
118 | 4. Update your resume with the new information
119 | 5. Save the changes back to GitHub
120 | 6. Provide a link to view your updated resume
121 | 
122 | ### Check Your Resume Status
123 | 
124 | Ask your AI assistant:
125 | ```
126 | "Can you check if I have a JSON Resume?"
127 | ```
128 | 
129 | The assistant will check if you have an existing resume and show its details.
130 | 
131 | ### Analyze Your Codebase
132 | 
133 | Ask your AI assistant:
134 | ```
135 | "What technologies am I using in this project?"
136 | ```
137 | 
138 | The assistant will analyze your codebase and provide insights about languages, technologies, and recent commits.
139 | 
140 | ## Configuration
141 | 
142 | The MCP server requires the following environment variables:
143 | 
144 | | Variable | Description |
145 | |----------|-------------|
146 | | `GITHUB_TOKEN` | Your GitHub personal access token with gist permissions |
147 | | `GITHUB_USERNAME` | Your GitHub username |
148 | | `OPENAI_API_KEY` | Your OpenAI API key |
149 | 
150 | ## Development
151 | 
152 | To run the server in development mode:
153 | 
154 | 1. Clone the repository:
155 | ```bash
156 | git clone https://github.com/jsonresume/mcp.git
157 | cd mcp
158 | ```
159 | 
160 | 2. Install dependencies:
161 | ```bash
162 | npm install
163 | ```
164 | 
165 | 3. Run in development mode:
166 | ```bash
167 | npm run dev
168 | ```
169 | 
170 | This starts the MCP server with the inspector tool for debugging.
171 | 
172 | ## Contributing
173 | 
174 | Contributions are welcome! Here's how you can contribute:
175 | 
176 | 1. Fork the repository
177 | 2. Create a feature branch: `git checkout -b feature/amazing-feature`
178 | 3. Commit your changes: `git commit -m 'Add some amazing feature'`
179 | 4. Push to the branch: `git push origin feature/amazing-feature`
180 | 5. Open a Pull Request
181 | 
182 | Please ensure your code follows the existing style and includes appropriate tests.
183 | 
184 | ## Testing
185 | 
186 | The MCP server includes several test scripts to help debug and verify functionality.
187 | 
188 | ### Running Tests
189 | 
190 | All test scripts are located in the `tests/` directory.
191 | 
192 | Before running tests, set your environment variables:
193 | 
194 | ```bash
195 | export GITHUB_TOKEN=your_github_token
196 | export OPENAI_API_KEY=your_openai_api_key
197 | export GITHUB_USERNAME=your_github_username
198 | ```
199 | 
200 | #### Check OpenAI API Key
201 | 
202 | Validates that your OpenAI API key is working correctly:
203 | 
204 | ```bash
205 | npx tsx tests/check-openai.ts
206 | ```
207 | 
208 | #### Mock Resume Enhancement
209 | 
210 | Tests the resume enhancement functionality using mock data (no API calls):
211 | 
212 | ```bash
213 | npx tsx tests/debug-mock.ts
214 | ```
215 | 
216 | #### Full Resume Enhancement Test
217 | 
218 | Tests the complete resume enhancement process with live API calls:
219 | 
220 | ```bash
221 | npx tsx tests/debug-enhance.ts
222 | ```
223 | 
224 | #### MCP Protocol Test
225 | 
226 | Tests the MCP server protocol communication:
227 | 
228 | ```bash
229 | node tests/test-mcp.js
230 | ```
231 | 
232 | ### Adding to package.json
233 | 
234 | For convenience, you can add these test commands to your package.json:
235 | 
236 | ```json
237 | "scripts": {
238 |   "test:openai": "tsx tests/check-openai.ts",
239 |   "test:mock": "tsx tests/debug-mock.ts",
240 |   "test:enhance": "tsx tests/debug-enhance.ts",
241 |   "test:mcp": "node tests/test-mcp.js"
242 | }
243 | ```
244 | 
245 | Then run them with `npm run test:mock`, etc.
246 | 
247 | ## License
248 | 
249 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
250 | 
251 | ## Acknowledgments
252 | 
253 | - [JSON Resume](https://jsonresume.org) for the resume standard
254 | - [Model Context Protocol](https://modelcontextprotocol.ai) for enabling AI tool integration
255 | - [OpenAI](https://openai.com) for powering the AI resume enhancements
256 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "Node16",
 6 |     "strict": true,
 7 |     "esModuleInterop": true,
 8 |     "skipLibCheck": true,
 9 |     "forceConsistentCasingInFileNames": true,
10 |     "resolveJsonModule": true,
11 |     "outDir": "./dist",
12 |     "rootDir": "."
13 |   },
14 |   "include": [
15 |     "./**/*.ts"
16 |   ],
17 |   "exclude": ["node_modules"]
18 | }
19 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | FROM node:lts-alpine
 3 | 
 4 | WORKDIR /app
 5 | 
 6 | # Copy package manifest
 7 | COPY package.json package-lock.json ./
 8 | 
 9 | # Install dependencies without running scripts (to skip potential issues)
10 | RUN npm install --ignore-scripts
11 | 
12 | # Copy all source files
13 | COPY . .
14 | 
15 | # Build the project using esbuild
16 | RUN npm run build
17 | 
18 | # Expose a port if needed (not specified, so omitted)
19 | 
20 | # Run the MCP server using the built artifact
21 | CMD ["node", "dist/index.cjs"]
22 | 
```

--------------------------------------------------------------------------------
/tests/check-openai.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import OpenAI from "openai";
 2 | 
 3 | // OpenAI key from environment
 4 | const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
 5 | 
 6 | async function checkOpenAI() {
 7 |   try {
 8 |     // Validate environment variable
 9 |     if (!OPENAI_API_KEY) {
10 |       throw new Error("OPENAI_API_KEY environment variable is required");
11 |     }
12 |     
13 |     console.log("Testing OpenAI API key validity...");
14 |     const openai = new OpenAI({ apiKey: OPENAI_API_KEY });
15 |     
16 |     // Try a simple completion request
17 |     const response = await openai.chat.completions.create({
18 |       model: "gpt-3.5-turbo",
19 |       messages: [{ role: "user", content: "Say hello" }],
20 |       max_tokens: 5
21 |     });
22 |     
23 |     console.log("OpenAI API key is valid!");
24 |     console.log("Response:", response.choices[0]?.message?.content);
25 |     return true;
26 |   } catch (error) {
27 |     console.log("Error testing OpenAI API key:", error);
28 |     return false;
29 |   }
30 | }
31 | 
32 | checkOpenAI();
33 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     required:
 9 |       - githubToken
10 |       - githubUsername
11 |       - openaiApiKey
12 |     properties:
13 |       githubToken:
14 |         type: string
15 |         description: Your GitHub personal access token with gist permissions.
16 |       githubUsername:
17 |         type: string
18 |         description: Your GitHub username.
19 |       openaiApiKey:
20 |         type: string
21 |         description: Your OpenAI API key.
22 |   commandFunction:
23 |     # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
24 |     |-
25 |     (config) => ({
26 |       command: 'node',
27 |       args: ['dist/index.cjs'],
28 |       env: {
29 |         GITHUB_TOKEN: config.githubToken,
30 |         GITHUB_USERNAME: config.githubUsername,
31 |         OPENAI_API_KEY: config.openaiApiKey
32 |       }
33 |     })
34 |   exampleConfig:
35 |     githubToken: your-github-token
36 |     githubUsername: your-github-username
37 |     openaiApiKey: your-openai-api-key
38 | 
```

--------------------------------------------------------------------------------
/src/schemas.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | 
 3 | // ISO date format validation (YYYY-MM-DD)
 4 | const isoDateSchema = z.string().regex(
 5 |   /^\d{4}-\d{2}-\d{2}$/,
 6 |   "Date must be in YYYY-MM-DD format"
 7 | );
 8 | 
 9 | // Project schema aligned with JSON Resume standard
10 | export const projectSchema = z.object({
11 |   name: z.string().min(1, "Project name is required"),
12 |   startDate: isoDateSchema,
13 |   endDate: isoDateSchema.optional(), // Optional for ongoing projects
14 |   description: z.string().min(10, "Description should be meaningful and professional"),
15 |   highlights: z.array(z.string()).optional(),
16 |   url: z.string().url("URL must be valid").optional(),
17 | });
18 | 
19 | // Skill schema with categorization support
20 | export const skillSchema = z.object({
21 |   name: z.string().min(1, "Skill name is required"),
22 |   level: z.string().optional(),
23 |   keywords: z.array(z.string()).optional(),
24 | });
25 | 
26 | // Resume update schema for OpenAI function calls
27 | export const resumeUpdateSchema = z.object({
28 |   newProject: projectSchema,
29 |   newSkills: z.array(skillSchema),
30 |   changes: z.array(z.string()),
31 | });
32 | 
33 | // Type definitions for the schemas
34 | export type Skill = z.infer<typeof skillSchema>;
35 | export type Project = z.infer<typeof projectSchema>;
36 | export type ResumeUpdate = z.infer<typeof resumeUpdateSchema>;
37 | 
```

--------------------------------------------------------------------------------
/src/tools.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Tool } from "@modelcontextprotocol/sdk/types.js";
 2 | 
 3 | // Define MCP tools
 4 | export const ANALYZE_CODEBASE_TOOL: Tool = {
 5 |   name: "github_analyze_codebase",
 6 |   description: "This is a tool from the github MCP server.\nAnalyzes the current codebase and returns information about technologies, languages, and recent commits",
 7 |   inputSchema: {
 8 |     type: "object",
 9 |     properties: {
10 |       directory: {
11 |         type: "string",
12 |         description: "The directory to analyze. If not provided, uses current working directory.",
13 |       },
14 |     },
15 |     required: [],
16 |   },
17 | };
18 | 
19 | export const CHECK_RESUME_TOOL: Tool = {
20 |   name: "github_check_resume",
21 |   description: "This is a tool from the github MCP server.\nChecks if a GitHub user has a JSON Resume and returns its information",
22 |   inputSchema: {
23 |     type: "object",
24 |     properties: {},
25 |     required: [],
26 |   },
27 | };
28 | 
29 | export const ENHANCE_RESUME_WITH_PROJECT_TOOL: Tool = {
30 |   name: "github_enhance_resume_with_project",
31 |   description: "This is a tool from the github MCP server.\nEnhances a GitHub user's JSON Resume with information about their current project",
32 |   inputSchema: {
33 |     type: "object",
34 |     properties: {
35 |       directory: {
36 |         type: "string",
37 |         description: "The directory of the project to analyze. If not provided, uses current working directory.",
38 |       },
39 |     },
40 |     required: [],
41 |   },
42 | };
43 | 
44 | // Export all tools as an array for convenience
45 | export const tools = [
46 |   ANALYZE_CODEBASE_TOOL,
47 |   CHECK_RESUME_TOOL,
48 |   ENHANCE_RESUME_WITH_PROJECT_TOOL,
49 | ];
50 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "@jsonresume/mcp",
 3 |   "version": "3.0.3",
 4 |   "description": "ModelContextProtocol server for enhancing JSON Resumes",
 5 |   "type": "module",
 6 |   "private": false,
 7 |   "scripts": {
 8 |     "make-executable": "node -e \"fs.chmodSync('dist/index.cjs', '755');\" --require fs",
 9 |     "build": "esbuild index.ts --outfile=dist/index.cjs --bundle --platform=node --format=cjs --banner:js='#!/usr/bin/env node' && npm run make-executable",
10 |     "watch": "esbuild index.ts --outfile=dist/index.cjs --bundle --platform=node --format=cjs --banner:js='#!/usr/bin/env node' --watch",
11 |     "inspect": "npx @modelcontextprotocol/inspector node dist/index.cjs",
12 |     "dev": "tsx index.ts",
13 |     "start": "node dist/index.cjs",
14 |     "start:stdio": "node dist/index.cjs stdio",
15 |     "start:http": "node dist/index.cjs",
16 |     "prepublishOnly": "npm run build",
17 |     "test:openai": "tsx tests/check-openai.ts",
18 |     "test:mock": "tsx tests/debug-mock.ts",
19 |     "test:enhance": "tsx tests/debug-enhance.ts",
20 |     "test:mcp": "node tests/test-mcp.js"
21 |   },
22 |   "bin": {
23 |     "@jsonresume/mcp": "./dist/index.cjs"
24 |   },
25 |   "files": [
26 |     "dist"
27 |   ],
28 |   "dependencies": {
29 |     "@hono/node-server": "^1.14.0",
30 |     "@modelcontextprotocol/sdk": "file:/home/ajax/repos/typescript-sdk",
31 |     "axios": "^1.8.1",
32 |     "dotenv": "^16.4.7",
33 |     "hono": "^4.7.5",
34 |     "octokit": "^3.2.1",
35 |     "openai": "^4.86.1"
36 |   },
37 |   "devDependencies": {
38 |     "@types/node": "^22.10.1",
39 |     "concurrently": "^8.2.2",
40 |     "esbuild": "^0.24.0",
41 |     "prettier": "^3.4.2",
42 |     "ts-node": "^10.9.2",
43 |     "tsx": "^4.19.3",
44 |     "typescript": "^5.3.3"
45 |   }
46 | }
47 | 
```

--------------------------------------------------------------------------------
/tests/debug-enhance.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { GitHubService } from "../src/github.js";
 2 | import { OpenAIService } from "../src/openai.js";
 3 | import { CodebaseAnalyzer } from "../src/codebase.js";
 4 | import { ResumeEnhancer } from "../src/resume-enhancer.js";
 5 | import { Resume } from "../src/types.js";
 6 | 
 7 | // Environment variables
 8 | const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
 9 | const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
10 | const GITHUB_USERNAME = process.env.GITHUB_USERNAME;
11 | 
12 | async function main() {
13 |   console.log("Starting resume enhancement test...");
14 |   
15 |   try {
16 |     // Validate environment variables
17 |     if (!GITHUB_TOKEN) {
18 |       throw new Error("GITHUB_TOKEN environment variable is required");
19 |     }
20 |     if (!OPENAI_API_KEY) {
21 |       throw new Error("OPENAI_API_KEY environment variable is required");
22 |     }
23 |     if (!GITHUB_USERNAME) {
24 |       throw new Error("GITHUB_USERNAME environment variable is required");
25 |     }
26 |     
27 |     // Initialize services
28 |     console.log("Initializing services...");
29 |     const githubService = new GitHubService(GITHUB_TOKEN, GITHUB_USERNAME);
30 |     const openaiService = new OpenAIService(OPENAI_API_KEY);
31 |     const codebaseAnalyzer = new CodebaseAnalyzer(process.cwd());
32 |     const resumeEnhancer = new ResumeEnhancer(openaiService);
33 |     
34 |     // Get or create a sample resume
35 |     console.log("Getting sample resume...");
36 |     const sampleResume: Resume = {
37 |       basics: {
38 |         name: "Test User",
39 |         label: "Software Developer",
40 |         email: "[email protected]"
41 |       },
42 |       skills: [],
43 |       projects: []
44 |     };
45 |     
46 |     // Analyze the codebase
47 |     console.log("Analyzing codebase...");
48 |     const codebaseAnalysis = await codebaseAnalyzer.analyze();
49 |     console.log("Codebase analysis complete:", {
50 |       repoName: codebaseAnalysis.repoName,
51 |       languages: Object.keys(codebaseAnalysis.languages || {}).join(", "),
52 |       technologies: (codebaseAnalysis.technologies || []).join(", ")
53 |     });
54 |     
55 |     // Enhance the resume
56 |     console.log("Enhancing resume...");
57 |     const enhancementResult = await resumeEnhancer.enhanceWithCurrentProject(
58 |       sampleResume,
59 |       codebaseAnalysis,
60 |       GITHUB_USERNAME
61 |     );
62 |     
63 |     console.log("Enhancement complete!");
64 |     console.log("Summary:", enhancementResult.summary);
65 |     console.log("Added skills:", enhancementResult.changes.addedSkills.join(", "));
66 |     console.log("Project name:", enhancementResult.changes.updatedProjects[0]);
67 |     
68 |     return enhancementResult;
69 |   } catch (error) {
70 |     console.log("Error during enhancement process:", error);
71 |     throw error;
72 |   }
73 | }
74 | 
75 | main().catch(error => {
76 |   console.log("Fatal error:", error);
77 |   process.exit(1);
78 | });
79 | 
```

--------------------------------------------------------------------------------
/tests/test-mcp.js:
--------------------------------------------------------------------------------

```javascript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { spawn } from "child_process";
  4 | import path from "path";
  5 | import { fileURLToPath } from "url";
  6 | 
  7 | // Get current file's directory
  8 | const __filename = fileURLToPath(import.meta.url);
  9 | const __dirname = path.dirname(__filename);
 10 | 
 11 | // Path to your MCP server executable
 12 | const mcpServerPath = path.resolve(__dirname, "../dist/index.cjs");
 13 | 
 14 | // Environment variables
 15 | const env = {
 16 |   ...process.env,
 17 |   GITHUB_TOKEN: process.env.GITHUB_TOKEN,
 18 |   OPENAI_API_KEY: process.env.OPENAI_API_KEY,
 19 |   GITHUB_USERNAME: process.env.GITHUB_USERNAME,
 20 | };
 21 | 
 22 | // Validate required environment variables
 23 | if (!env.GITHUB_TOKEN) {
 24 |   console.log("Error: GITHUB_TOKEN environment variable is required");
 25 |   process.exit(1);
 26 | }
 27 | 
 28 | if (!env.OPENAI_API_KEY) {
 29 |   console.log("Error: OPENAI_API_KEY environment variable is required");
 30 |   process.exit(1);
 31 | }
 32 | 
 33 | if (!env.GITHUB_USERNAME) {
 34 |   console.log("Error: GITHUB_USERNAME environment variable is required");
 35 |   process.exit(1);
 36 | }
 37 | 
 38 | // Spawn the MCP server
 39 | const mcp = spawn("node", [mcpServerPath], { env });
 40 | 
 41 | // Listen for stdout (MCP responses)
 42 | mcp.stdout.on("data", (data) => {
 43 |   const message = data.toString().trim();
 44 |   try {
 45 |     // Try to parse JSON
 46 |     const jsonMessage = JSON.parse(message);
 47 |     console.log("Received MCP response:", JSON.stringify(jsonMessage, null, 2));
 48 | 
 49 |     // If this is the ListTools response, send a CallTool request
 50 |     if (jsonMessage.result && jsonMessage.result.tools) {
 51 |       console.log("Tools available, sending CallTool request...");
 52 |       sendCallToolRequest();
 53 |     }
 54 |   } catch (e) {
 55 |     // Not JSON, just log the message
 56 |     console.log("MCP server stdout:", message);
 57 |   }
 58 | });
 59 | 
 60 | // Listen for stderr (console.log messages from the MCP server)
 61 | mcp.stderr.on("data", (data) => {
 62 |   console.log("MCP server stderr:", data.toString().trim());
 63 | });
 64 | 
 65 | // When the MCP server exits
 66 | mcp.on("close", (code) => {
 67 |   console.log(`MCP server exited with code ${code}`);
 68 | });
 69 | 
 70 | // Send a JSON-RPC message to the MCP server
 71 | function sendMessage(message) {
 72 |   mcp.stdin.write(JSON.stringify(message) + "\n");
 73 | }
 74 | 
 75 | // Wait for server to start then send ListTools
 76 | setTimeout(() => {
 77 |   console.log("Sending ListTools request...");
 78 |   sendMessage({
 79 |     jsonrpc: "2.0",
 80 |     id: "1",
 81 |     method: "listTools",
 82 |     params: {},
 83 |   });
 84 | }, 1000);
 85 | 
 86 | // Send a CallTool request
 87 | function sendCallToolRequest() {
 88 |   console.log("Sending CallTool request for github_enhance_resume_with_project...");
 89 |   sendMessage({
 90 |     jsonrpc: "2.0",
 91 |     id: "2",
 92 |     method: "callTool",
 93 |     params: {
 94 |       name: "github_enhance_resume_with_project",
 95 |       arguments: {
 96 |         directory: process.cwd(),
 97 |       },
 98 |     },
 99 |   });
100 | }
101 | 
102 | // Exit gracefully
103 | process.on("SIGINT", () => {
104 |   console.log("Exiting...");
105 |   mcp.kill();
106 |   process.exit(0);
107 | });
108 | 
```

--------------------------------------------------------------------------------
/src/resume-enhancer.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Resume } from './types.js';
 2 | import { OpenAIService } from './openai.js';
 3 | import { CodebaseAnalysisResult } from './codebase.js';
 4 | 
 5 | export interface EnhancementResult {
 6 |   updatedResume: Resume;
 7 |   changes: {
 8 |     addedSkills: string[];
 9 |     updatedProjects: string[];
10 |     updatedWork: string[];
11 |     otherChanges: string[];
12 |   };
13 |   summary: string;
14 |   userMessage?: string;
15 |   resumeLink?: string;
16 | }
17 | 
18 | export class ResumeEnhancer {
19 |   private openAIService: OpenAIService;
20 |   
21 |   constructor(openAIService: OpenAIService) {
22 |     this.openAIService = openAIService;
23 |   }
24 |   
25 |   /**
26 |    * Enhance a resume with details about the current project
27 |    */
28 |   async enhanceWithCurrentProject(
29 |     resume: Resume,
30 |     codebaseAnalysis: CodebaseAnalysisResult,
31 |     githubUsername: string
32 |   ): Promise<EnhancementResult> {
33 |     try {
34 |       console.log('Starting resume enhancement with codebase details:', JSON.stringify({
35 |         repoName: codebaseAnalysis.repoName,
36 |         languages: Object.keys(codebaseAnalysis.languages),
37 |         technologies: codebaseAnalysis.technologies
38 |       }));
39 |       
40 |       // Get resume updates from OpenAI
41 |       console.log('Calling OpenAI to generate resume enhancement...');
42 |       const update = await this.openAIService.generateResumeEnhancement(codebaseAnalysis);
43 |       console.log('Received resume updates from OpenAI');
44 |       
45 |       // Apply the updates to the resume
46 |       console.log('Enhancing resume with new data...');
47 |       const updatedResume = await this.openAIService.enhanceResume(resume, update);
48 |       console.log('Resume enhanced successfully');
49 |       
50 |       // Generate a summary of the changes
51 |       console.log('Generating update summary...');
52 |       const summary = await this.openAIService.generateUpdateSummary(update.changes);
53 |       console.log('Summary generated');
54 |       
55 |       // Create user message
56 |       const userMessage = this.createUserMessage(githubUsername, update.changes);
57 |       
58 |       return {
59 |         updatedResume,
60 |         changes: {
61 |           addedSkills: update.newSkills.map(s => s.name),
62 |           updatedProjects: [update.newProject.name],
63 |           updatedWork: [],
64 |           otherChanges: update.changes
65 |         },
66 |         summary,
67 |         userMessage,
68 |         resumeLink: `https://registry.jsonresume.org/${githubUsername}`
69 |       };
70 |     } catch (error) {
71 |       console.log('Error enhancing resume with current project:', error);
72 |       throw error;
73 |     }
74 |   }
75 |   
76 |   /**
77 |    * Create a user-friendly message with details about the updates and a link to the resume
78 |    */
79 |   private createUserMessage(username: string, changes: string[]): string {
80 |     return `Your resume has been updated with your latest project contributions! View it at https://registry.jsonresume.org/${username}\n\nChanges made:\n${changes.map(c => `- ${c}`).join('\n')}\n\n⚠️ Note: Please review the changes to ensure they match your preferences. You can revert to a previous version through your GitHub Gist revision history if needed.`;
81 |   }
82 | }
83 | 
```

--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // JSON Resume schema types based on https://jsonresume.org/schema/
  2 | 
  3 | export interface Resume {
  4 |   basics?: Basics;
  5 |   work?: Work[];
  6 |   volunteer?: Volunteer[];
  7 |   education?: Education[];
  8 |   awards?: Award[];
  9 |   certificates?: Certificate[];
 10 |   publications?: Publication[];
 11 |   skills?: Skill[];
 12 |   languages?: Language[];
 13 |   interests?: Interest[];
 14 |   references?: Reference[];
 15 |   projects?: Project[];
 16 |   meta?: Meta;
 17 | }
 18 | 
 19 | export interface Basics {
 20 |   name?: string;
 21 |   label?: string;
 22 |   image?: string;
 23 |   email?: string;
 24 |   phone?: string;
 25 |   url?: string;
 26 |   summary?: string;
 27 |   location?: Location;
 28 |   profiles?: Profile[];
 29 | }
 30 | 
 31 | export interface Location {
 32 |   address?: string;
 33 |   postalCode?: string;
 34 |   city?: string;
 35 |   countryCode?: string;
 36 |   region?: string;
 37 | }
 38 | 
 39 | export interface Profile {
 40 |   network?: string;
 41 |   username?: string;
 42 |   url?: string;
 43 | }
 44 | 
 45 | export interface Work {
 46 |   name?: string;
 47 |   position?: string;
 48 |   url?: string;
 49 |   startDate?: string;
 50 |   endDate?: string;
 51 |   summary?: string;
 52 |   highlights?: string[];
 53 |   location?: string;
 54 | }
 55 | 
 56 | export interface Volunteer {
 57 |   organization?: string;
 58 |   position?: string;
 59 |   url?: string;
 60 |   startDate?: string;
 61 |   endDate?: string;
 62 |   summary?: string;
 63 |   highlights?: string[];
 64 | }
 65 | 
 66 | export interface Education {
 67 |   institution?: string;
 68 |   url?: string;
 69 |   area?: string;
 70 |   studyType?: string;
 71 |   startDate?: string;
 72 |   endDate?: string;
 73 |   score?: string;
 74 |   courses?: string[];
 75 | }
 76 | 
 77 | export interface Award {
 78 |   title?: string;
 79 |   date?: string;
 80 |   awarder?: string;
 81 |   summary?: string;
 82 | }
 83 | 
 84 | export interface Certificate {
 85 |   name?: string;
 86 |   date?: string;
 87 |   issuer?: string;
 88 |   url?: string;
 89 | }
 90 | 
 91 | export interface Publication {
 92 |   name?: string;
 93 |   publisher?: string;
 94 |   releaseDate?: string;
 95 |   url?: string;
 96 |   summary?: string;
 97 | }
 98 | 
 99 | export interface Skill {
100 |   name?: string;
101 |   level?: string;
102 |   keywords?: string[];
103 |   category?: string; // Added for skills grouping
104 | }
105 | 
106 | export interface Language {
107 |   language?: string;
108 |   fluency?: string;
109 | }
110 | 
111 | export interface Interest {
112 |   name?: string;
113 |   keywords?: string[];
114 | }
115 | 
116 | export interface Reference {
117 |   name?: string;
118 |   reference?: string;
119 | }
120 | 
121 | export interface Project {
122 |   name?: string;
123 |   description?: string;
124 |   highlights?: string[];
125 |   keywords?: string[];
126 |   startDate?: string;
127 |   endDate?: string;
128 |   url?: string;
129 |   roles?: string[];
130 |   entity?: string;
131 |   type?: string;
132 | }
133 | 
134 | export interface Meta {
135 |   canonical?: string;
136 |   version?: string;
137 |   lastModified?: string;
138 | }
139 | 
140 | // Sample resume template
141 | export const sampleResume: Resume = {
142 |   basics: {
143 |     name: "",
144 |     label: "Software Developer",
145 |     email: "",
146 |     phone: "",
147 |     summary: "Experienced software developer with a passion for creating efficient and scalable applications.",
148 |     location: {
149 |       city: "",
150 |       countryCode: "",
151 |       region: ""
152 |     },
153 |     profiles: [
154 |       {
155 |         network: "GitHub",
156 |         username: "",
157 |         url: ""
158 |       },
159 |       {
160 |         network: "LinkedIn",
161 |         username: "",
162 |         url: ""
163 |       }
164 |     ]
165 |   },
166 |   work: [],
167 |   education: [],
168 |   skills: [],
169 |   projects: [],
170 |   meta: {
171 |     version: "v1.0.0",
172 |     lastModified: new Date().toISOString()
173 |   }
174 | };
175 | 
```

--------------------------------------------------------------------------------
/tests/test-direct.js:
--------------------------------------------------------------------------------

```javascript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { GitHubService } from "./src/github.ts";
  4 | import { OpenAIService } from "./src/openai.ts";
  5 | import { CodebaseAnalyzer } from "./src/codebase.ts";
  6 | import { ResumeEnhancer } from "./src/resume-enhancer.ts";
  7 | 
  8 | // Environment variables
  9 | const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
 10 | const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
 11 | const GITHUB_USERNAME = process.env.GITHUB_USERNAME;
 12 | 
 13 | // Initialize services
 14 | async function init() {
 15 |   try {
 16 |     if (!GITHUB_TOKEN) {
 17 |       throw new Error("GITHUB_TOKEN environment variable is required");
 18 |     }
 19 |     if (!OPENAI_API_KEY) {
 20 |       throw new Error("OPENAI_API_KEY environment variable is required");
 21 |     }
 22 |     if (!GITHUB_USERNAME) {
 23 |       throw new Error("GITHUB_USERNAME environment variable is required");
 24 |     }
 25 | 
 26 |     console.log("Initializing services...");
 27 |     const githubService = new GitHubService(GITHUB_TOKEN, GITHUB_USERNAME);
 28 |     const openaiService = new OpenAIService(OPENAI_API_KEY);
 29 |     const codebaseAnalyzer = new CodebaseAnalyzer(process.cwd());
 30 |     const resumeEnhancer = new ResumeEnhancer(openaiService);
 31 | 
 32 |     console.log("Services initialized successfully");
 33 | 
 34 |     return { githubService, openaiService, codebaseAnalyzer, resumeEnhancer };
 35 |   } catch (error) {
 36 |     console.log("Error initializing services:", error);
 37 |     process.exit(1);
 38 |   }
 39 | }
 40 | 
 41 | async function enhanceResumeWithProject() {
 42 |   try {
 43 |     console.log("Starting resume enhancement with current project...");
 44 | 
 45 |     const { githubService, openaiService, codebaseAnalyzer, resumeEnhancer } = await init();
 46 | 
 47 |     // Step 1: Fetch the user's resume from GitHub gists
 48 |     console.log("Fetching resume from GitHub gists...");
 49 |     let resume = await githubService.getResumeFromGists();
 50 | 
 51 |     if (!resume) {
 52 |       // If no resume exists, create a sample one
 53 |       console.log("No resume found, creating a sample resume...");
 54 |       const userProfile = await githubService.getUserProfile();
 55 |       resume = await githubService.createSampleResume();
 56 |       console.log("Sample resume created successfully");
 57 |     } else {
 58 |       console.log("Existing resume found");
 59 |     }
 60 | 
 61 |     // Step 2: Analyze the current codebase
 62 |     console.log("Analyzing current project...");
 63 |     const codebaseAnalysis = await codebaseAnalyzer.analyze();
 64 |     console.log(
 65 |       "Codebase analysis completed:",
 66 |       JSON.stringify({
 67 |         repoName: codebaseAnalysis.repoName,
 68 |         languages: Object.keys(codebaseAnalysis.languages),
 69 |         technologies: codebaseAnalysis.technologies,
 70 |       })
 71 |     );
 72 | 
 73 |     // Step 3: Enhance the resume with the current project
 74 |     console.log("Enhancing resume with current project...");
 75 |     const { updatedResume, changes, summary, userMessage, resumeLink } =
 76 |       await resumeEnhancer.enhanceWithCurrentProject(resume, codebaseAnalysis, GITHUB_USERNAME);
 77 | 
 78 |     console.log("Resume enhancement completed successfully");
 79 |     console.log("Summary:", summary);
 80 | 
 81 |     // Step 4: Update the resume on GitHub
 82 |     console.log("Updating resume on GitHub...");
 83 |     const finalResume = await githubService.updateResume(updatedResume);
 84 | 
 85 |     return {
 86 |       message: "Resume enhanced with current project successfully",
 87 |       changes: changes,
 88 |       summary,
 89 |       userMessage,
 90 |       resumeUrl: resumeLink || `https://registry.jsonresume.org/${GITHUB_USERNAME}`,
 91 |       projectName: codebaseAnalysis.repoName,
 92 |     };
 93 |   } catch (error) {
 94 |     console.log("Error enhancing resume with project:", error);
 95 |     throw error;
 96 |   }
 97 | }
 98 | 
 99 | enhanceResumeWithProject()
100 |   .then((result) => {
101 |     console.log("Result:", JSON.stringify(result, null, 2));
102 |     process.exit(0);
103 |   })
104 |   .catch((error) => {
105 |     console.log("Fatal error:", error);
106 |     process.exit(1);
107 |   });
108 | 
```

--------------------------------------------------------------------------------
/tests/debug-mock.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ResumeEnhancer } from "../src/resume-enhancer.js";
  2 | import { CodebaseAnalysisResult } from "../src/codebase.js";
  3 | import { Resume } from "../src/types.js";
  4 | import { ResumeUpdate } from "../src/schemas.js";
  5 | 
  6 | // Mock OpenAI service
  7 | class MockOpenAIService {
  8 |   async generateResumeEnhancement(): Promise<ResumeUpdate> {
  9 |     return {
 10 |       newProject: {
 11 |         name: "JSON Resume MCP Server",
 12 |         startDate: "2024-12",
 13 |         endDate: "Present",
 14 |         description: "Developed a Model Context Protocol (MCP) server that enhances AI assistants with the ability to update a JSON Resume by analyzing coding projects.",
 15 |         highlights: [
 16 |           "Implemented TypeScript/Node.js MCP server integration",
 17 |           "Created GitHub API integration for resume storage and retrieval",
 18 |           "Developed OpenAI function calling for intelligent resume enhancement",
 19 |           "Implemented codebase analysis tools for skills and project detection"
 20 |         ],
 21 |         url: "https://github.com/jsonresume/mcp-starter"
 22 |       },
 23 |       newSkills: [
 24 |         {
 25 |           name: "TypeScript",
 26 |           level: "Advanced",
 27 |           keywords: ["Node.js", "Static Typing", "ES6+"]
 28 |         },
 29 |         {
 30 |           name: "Model Context Protocol (MCP)",
 31 |           level: "Intermediate",
 32 |           keywords: ["AI Integration", "Function Calling", "API Design"]
 33 |         },
 34 |         {
 35 |           name: "GitHub API",
 36 |           level: "Intermediate",
 37 |           keywords: ["OAuth", "REST API", "Gist Management"]
 38 |         }
 39 |       ],
 40 |       changes: [
 41 |         "Added JSON Resume MCP Server project",
 42 |         "Added TypeScript skill with Node.js, Static Typing, ES6+ keywords",
 43 |         "Added Model Context Protocol (MCP) skill with AI Integration, Function Calling, API Design keywords",
 44 |         "Added GitHub API skill with OAuth, REST API, Gist Management keywords"
 45 |       ]
 46 |     };
 47 |   }
 48 | 
 49 |   async enhanceResume(resume: Resume, update: ResumeUpdate): Promise<Resume> {
 50 |     const result = { ...resume };
 51 |     
 52 |     // Add the new project
 53 |     result.projects = [...(result.projects || []), update.newProject];
 54 |     
 55 |     // Add new skills
 56 |     result.skills = [...(result.skills || []), ...update.newSkills];
 57 |     
 58 |     return result;
 59 |   }
 60 | 
 61 |   async generateUpdateSummary(changes: string[]): Promise<string> {
 62 |     return "Your resume has been enhanced with a new JSON Resume MCP Server project and skills in TypeScript, Model Context Protocol (MCP), and GitHub API.";
 63 |   }
 64 | }
 65 | 
 66 | async function testEnhanceWithMock() {
 67 |   try {
 68 |     // Create a sample resume
 69 |     const resume: Resume = {
 70 |       basics: {
 71 |         name: "Test User",
 72 |         label: "Software Developer",
 73 |         email: "[email protected]"
 74 |       },
 75 |       skills: [],
 76 |       projects: []
 77 |     };
 78 |     
 79 |     // Create a sample codebase analysis
 80 |     const codebaseAnalysis: CodebaseAnalysisResult = {
 81 |       repoName: "mcp-server",
 82 |       languages: {
 83 |         "TypeScript": 80,
 84 |         "Markdown": 15,
 85 |         "JSON": 5
 86 |       },
 87 |       fileCount: 10,
 88 |       recentCommits: [],
 89 |       technologies: ["Node.js", "TypeScript", "GitHub API", "OpenAI"],
 90 |       summary: "A Model Context Protocol server for enhancing resumes"
 91 |     };
 92 |     
 93 |     // Create the resume enhancer with the mock OpenAI service
 94 |     const mockOpenAIService = new MockOpenAIService();
 95 |     const resumeEnhancer = new ResumeEnhancer(mockOpenAIService as any);
 96 |     
 97 |     console.log("Enhancing resume with mock data...");
 98 |     const result = await resumeEnhancer.enhanceWithCurrentProject(
 99 |       resume,
100 |       codebaseAnalysis,
101 |       "testuser"
102 |     );
103 |     
104 |     console.log("Enhancement successful!");
105 |     console.log("Summary:", result.summary);
106 |     console.log("Added skills:", result.changes.addedSkills.join(", "));
107 |     console.log("Updated projects:", result.changes.updatedProjects.join(", "));
108 |     
109 |     return result;
110 |   } catch (error) {
111 |     console.log("Error in mock test:", error);
112 |     throw error;
113 |   }
114 | }
115 | 
116 | testEnhanceWithMock()
117 |   .then(result => {
118 |     console.log("Test completed successfully");
119 |     process.exit(0);
120 |   })
121 |   .catch(error => {
122 |     console.log("Test failed:", error);
123 |     process.exit(1);
124 |   });
125 | 
```

--------------------------------------------------------------------------------
/apps/registry/scripts/jobs/recommendations/product-requirements.md:
--------------------------------------------------------------------------------

```markdown
  1 | # JSON Resume Recommendation Engine
  2 | 
  3 | ## Overview
  4 | 
  5 | The JSON Resume Recommendation Engine is a feature that analyzes a user's resume and current project to automatically enhance their resume with relevant skills, project details, and work experiences. This document outlines the requirements, architecture, and implementation details for this feature.
  6 | 
  7 | ## Key Features
  8 | 
  9 | 1. **Resume Analysis**: Analyzes existing JSON Resume content to understand the user's background
 10 | 2. **Codebase Analysis**: Examines the user's current project to extract technologies, languages, and contributions
 11 | 3. **Automatic Enhancement**:
 12 |    - Adds new skills based on technologies used in projects
 13 |    - Updates project details with current work
 14 |    - Makes recommendations for improving work experience descriptions
 15 |    - Validates against the JSON Resume schema to ensure compatibility
 16 | 4. **User Experience**:
 17 |    - Provides a detailed summary of all changes made
 18 |    - Generates a direct link to view the updated resume
 19 |    - Includes warning about potential issues and how to revert changes
 20 |    - Maintains full resume.json output following JSON schema
 21 | 
 22 | ## Architecture
 23 | 
 24 | The recommendation engine is built with the following components:
 25 | 
 26 | 1. **Resume Enhancer**: Core class that orchestrates the enhancement process
 27 | 2. **GitHub Service**: Handles resume retrieval and update through GitHub Gists
 28 | 3. **Codebase Analyzer**: Extracts technologies, languages, and commit history from current project
 29 | 4. **OpenAI Service**: Leverages AI to generate high-quality resume enhancements
 30 | 5. **Schema Validation**: Ensures all modifications follow the JSON Resume schema
 31 | 
 32 | ## Implementation Details
 33 | 
 34 | ### ResumeEnhancer Class
 35 | - Extracts project information from codebase analysis
 36 | - Maps technologies to skill categories
 37 | - Processes changes through OpenAI
 38 | - Validates schema compliance
 39 | - Creates user-friendly output with detailed change information
 40 | 
 41 | ### OpenAI Integration
 42 | - Generates new project data for the current codebase
 43 | - Identifies new skills from the codebase that aren't in the existing resume
 44 | - Strictly additive approach - never modifies or removes existing content
 45 | - Ensures professional tone and formatting in resume content
 46 | - Maintains consistency with existing resume by only adding complementary information
 47 | 
 48 | ### Data Preservation Principles
 49 | - All existing resume data is preserved without modification
 50 | - New projects are only added if they don't already exist (checked by name)
 51 | - New skills are only added if they don't already exist in the resume
 52 | - Original formatting and structure of the resume is maintained
 53 | - Special properties (like _gistId) are preserved during enhancement
 54 | 
 55 | ### Schema Validation
 56 | - Validates all modifications against JSON Resume schema
 57 | - Prevents invalid data structures or missing required fields
 58 | - Ensures backward compatibility with existing tools
 59 | 
 60 | ### User Communication
 61 | - Provides markdown-formatted output with changes grouped by category
 62 | - Includes direct link to view updated resume
 63 | - Warns about potential issues and explains how to revert changes if needed
 64 | 
 65 | ## Technical Requirements
 66 | 
 67 | 1. TypeScript for type safety and improved development experience
 68 | 2. GitHub API integration for resume storage and retrieval
 69 | 3. OpenAI API for intelligent content generation
 70 | 4. Schema validation to ensure compatibility
 71 | 5. Error handling and logging for troubleshooting
 72 | 
 73 | ## Implementation Notes
 74 | 
 75 | ### OpenAIService Enhancements
 76 | 
 77 | The OpenAIService has been updated to use a more focused approach for resume enhancement:
 78 | 
 79 | 1. **Targeted Prompt Design**:
 80 |    - Prompt now explicitly requests only a single new project and new skills
 81 |    - No mention of updating or modifying existing content
 82 |    - Clear instructions on what should and should not be generated
 83 | 
 84 | 2. **Data Processing Changes**:
 85 |    - Added a dedicated `addProjectAndSkills` method that only adds new content
 86 |    - Pre-validation of existing projects to avoid duplicates
 87 |    - Strict filtering of skills to only add ones that don't already exist
 88 |    - Preserves special properties like `_gistId` during processing
 89 | 
 90 | 3. **Response Structure**:
 91 |    - OpenAI now responds with specific `newProject` and `newSkills` properties
 92 |    - No full resume returned, only the specific additions
 93 |    - Includes a changes array describing what was added for user transparency
 94 | 
 95 | This approach ensures that resume enhancement is completely non-destructive and only adds relevant new information based on the user's current project.
 96 | 
 97 | ## Future Enhancements
 98 | 
 99 | 1. **Customization Options**: Allow users to control enhancement parameters
100 | 2. **Multi-project Analysis**: Analyze multiple projects for comprehensive skills extraction
101 | 3. **Historical Analysis**: Consider commit history and contribution patterns
102 | 4. **Industry-specific Recommendations**: Tailor enhancements based on industry norms
103 | 5. **Resume Optimization Scoring**: Provide metrics on resume completeness and quality
104 | 
```

--------------------------------------------------------------------------------
/src/github.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Octokit } from "octokit";
  2 | import { Resume, sampleResume } from "./types.js";
  3 | 
  4 | export class GitHubService {
  5 |   private octokit: Octokit;
  6 |   private username: string;
  7 |   private cachedGistId: string | null = null;
  8 | 
  9 |   constructor(token: string, username: string) {
 10 |     if (!token) {
 11 |       throw new Error("GitHub token is required");
 12 |     }
 13 |     if (!username) {
 14 |       throw new Error("GitHub username is required");
 15 |     }
 16 | 
 17 |     this.octokit = new Octokit({ auth: token });
 18 |     this.username = username;
 19 |   }
 20 | 
 21 |   /**
 22 |    * Fetch user profile information from GitHub
 23 |    */
 24 |   async getUserProfile() {
 25 |     try {
 26 |       const { data } = await this.octokit.rest.users.getByUsername({
 27 |         username: this.username,
 28 |       });
 29 |       return data;
 30 |     } catch (error) {
 31 |       console.log("Error fetching user profile:", error);
 32 |       throw error;
 33 |     }
 34 |   }
 35 | 
 36 |   /**
 37 |    * Get resume.json from user's gists
 38 |    */
 39 |   async getResumeFromGists(): Promise<Resume | null> {
 40 |     try {
 41 |       // If we've already found the gist ID in this session, use it
 42 |       if (this.cachedGistId) {
 43 |         console.log(`Using cached gist ID: ${this.cachedGistId}`);
 44 |         try {
 45 |           const { data: gist } = await this.octokit.rest.gists.get({
 46 |             gist_id: this.cachedGistId,
 47 |           });
 48 |           
 49 |           const files = gist.files || {};
 50 |           const resumeFile = Object.values(files).find(
 51 |             (file) => file?.filename === "resume.json"
 52 |           );
 53 | 
 54 |           if (resumeFile && resumeFile.raw_url) {
 55 |             const response = await fetch(resumeFile.raw_url);
 56 |             const resumeData = await response.json();
 57 |             console.log("Successfully fetched resume from cached gist ID");
 58 |             return { 
 59 |               ...resumeData,
 60 |               _gistId: this.cachedGistId
 61 |             };
 62 |           }
 63 |         } catch (error) {
 64 |           console.log("Error fetching from cached gist ID, will try listing all gists:", error);
 65 |           this.cachedGistId = null;
 66 |         }
 67 |       }
 68 | 
 69 |       // List all gists for the user
 70 |       console.log(`Listing gists for user: ${this.username}`);
 71 |       const { data: gists } = await this.octokit.rest.gists.list({
 72 |         username: this.username,
 73 |         per_page: 100,
 74 |       });
 75 |       
 76 |       console.log(`Found ${gists.length} gists, searching for resume.json`);
 77 | 
 78 |       // Find all gists containing resume.json and sort by updated_at
 79 |       const resumeGists = gists
 80 |         .filter(gist => {
 81 |           const files = gist.files || {};
 82 |           return Object.values(files).some(
 83 |             (file) => file?.filename === "resume.json"
 84 |           );
 85 |         })
 86 |         .sort((a, b) => {
 87 |           // Sort by updated_at in descending order (newest first)
 88 |           return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
 89 |         });
 90 | 
 91 |       if (resumeGists.length > 0) {
 92 |         // Use the most recently updated resume.json gist
 93 |         const mostRecentGist = resumeGists[0];
 94 |         console.log(`Found ${resumeGists.length} resume.json gists. Using most recent: ${mostRecentGist.id} (updated: ${mostRecentGist.updated_at})`);
 95 |         
 96 |         // Cache the gist ID for future use
 97 |         this.cachedGistId = mostRecentGist.id;
 98 |         
 99 |         const files = mostRecentGist.files || {};
100 |         const resumeFile = Object.values(files).find(
101 |           (file) => file?.filename === "resume.json"
102 |         );
103 |         
104 |         if (resumeFile && resumeFile.raw_url) {
105 |           // Fetch the content of resume.json
106 |           const response = await fetch(resumeFile.raw_url);
107 |           const resumeData = await response.json();
108 |           return { 
109 |             ...resumeData,
110 |             _gistId: mostRecentGist.id // Store the gist ID for later updates
111 |           };
112 |         }
113 |       }
114 | 
115 |       console.log("No resume.json found in any gists");
116 |       return null; // No resume.json found
117 |     } catch (error) {
118 |       console.log("Error fetching resume from gists:", error);
119 |       throw error;
120 |     }
121 |   }
122 | 
123 |   /**
124 |    * Create a sample resume.json gist if none exists
125 |    */
126 |   async createSampleResume(): Promise<Resume> {
127 |     try {
128 |       // Get user profile to populate some basic fields
129 |       const userProfile = await this.getUserProfile();
130 |       
131 |       // Create a copy of the sample resume and populate with GitHub profile info
132 |       const newResume = JSON.parse(JSON.stringify(sampleResume)) as Resume;
133 |       
134 |       if (newResume.basics) {
135 |         newResume.basics.name = userProfile.name || this.username;
136 |         
137 |         if (newResume.basics.profiles) {
138 |           const githubProfile = newResume.basics.profiles.find(p => p.network === "GitHub");
139 |           if (githubProfile) {
140 |             githubProfile.username = this.username;
141 |             githubProfile.url = `https://github.com/${this.username}`;
142 |           }
143 |         }
144 |         
145 |         newResume.basics.email = userProfile.email || "";
146 |       }
147 |       
148 |       console.log("Creating new gist with resume.json");
149 |       // Create a new gist with resume.json
150 |       const { data: gist } = await this.octokit.rest.gists.create({
151 |         files: {
152 |           "resume.json": {
153 |             content: JSON.stringify(newResume, null, 2),
154 |           },
155 |         },
156 |         description: "My JSON Resume",
157 |         public: true,
158 |       });
159 |       
160 |       // Cache the gist ID for future use
161 |       this.cachedGistId = gist.id;
162 |       console.log(`Created new gist with ID: ${gist.id}`);
163 |       
164 |       return { 
165 |         ...newResume,
166 |         _gistId: gist.id
167 |       };
168 |     } catch (error) {
169 |       console.log("Error creating sample resume:", error);
170 |       throw error;
171 |     }
172 |   }
173 | 
174 |   /**
175 |    * Update an existing resume.json gist
176 |    */
177 |   async updateResume(resume: Resume): Promise<Resume> {
178 |     try {
179 |       // Check if there's a _gistId in the resume
180 |       let gistId = (resume as any)._gistId;
181 |       
182 |       // If not, check our cached gist ID
183 |       if (!gistId && this.cachedGistId) {
184 |         console.log(`No _gistId in resume object, using cached gistId: ${this.cachedGistId}`);
185 |         gistId = this.cachedGistId;
186 |       }
187 |       
188 |       // If we still don't have a gist ID, try to find it
189 |       if (!gistId) {
190 |         console.log("No gist ID found, attempting to find existing resume");
191 |         const existingResume = await this.getResumeFromGists();
192 |         if (existingResume && (existingResume as any)._gistId) {
193 |           gistId = (existingResume as any)._gistId;
194 |           console.log(`Found existing resume with gist ID: ${gistId}`);
195 |         }
196 |       }
197 |       
198 |       // If we still don't have a gist ID, create a new gist
199 |       if (!gistId) {
200 |         console.log("No existing resume found, creating a new one");
201 |         const newResume = await this.createSampleResume();
202 |         gistId = (newResume as any)._gistId;
203 |         
204 |         // Merge the existing resume data with our new resume
205 |         resume = {
206 |           ...newResume,
207 |           ...resume,
208 |           _gistId: gistId
209 |         };
210 |       }
211 |       
212 |       // Create a clean copy of the resume without the _gistId property
213 |       const { _gistId, ...resumeData } = { ...resume, _gistId: undefined };
214 |       
215 |       // Update the lastModified date
216 |       if (resumeData.meta) {
217 |         resumeData.meta.lastModified = new Date().toISOString();
218 |       } else {
219 |         resumeData.meta = {
220 |           lastModified: new Date().toISOString()
221 |         };
222 |       }
223 |       
224 |       console.log(`Updating gist with ID: ${gistId}`);
225 |       // Update the gist
226 |       const { data: updatedGist } = await this.octokit.rest.gists.update({
227 |         gist_id: gistId as string,
228 |         files: {
229 |           "resume.json": {
230 |             content: JSON.stringify(resumeData, null, 2),
231 |           },
232 |         },
233 |       });
234 |       
235 |       // Cache the gist ID for future use
236 |       this.cachedGistId = gistId;
237 |       
238 |       return { 
239 |         ...resumeData,
240 |         _gistId: gistId
241 |       };
242 |     } catch (error) {
243 |       console.log("Error updating resume gist:", error);
244 |       throw error;
245 |     }
246 |   }
247 | 
248 |   /**
249 |    * Get user's repositories and their contributions
250 |    */
251 |   async getUserRepositories() {
252 |     try {
253 |       // Get user's repositories
254 |       const { data: repos } = await this.octokit.rest.repos.listForUser({
255 |         username: this.username,
256 |         sort: "updated",
257 |         per_page: 10, // Limit to recent 10 repos
258 |       });
259 |       
260 |       return repos;
261 |     } catch (error) {
262 |       console.log("Error fetching user repositories:", error);
263 |       throw error;
264 |     }
265 |   }
266 | 
267 |   /**
268 |    * Get user's contributions to a specific repository
269 |    */
270 |   async getRepoContributions(owner: string, repo: string) {
271 |     try {
272 |       // Get user's commits to the repository
273 |       const { data: commits } = await this.octokit.rest.repos.listCommits({
274 |         owner,
275 |         repo,
276 |         author: this.username,
277 |         per_page: 20,
278 |       });
279 |       
280 |       return commits;
281 |     } catch (error) {
282 |       console.log(`Error fetching contributions to ${owner}/${repo}:`, error);
283 |       return []; // Return empty array on error
284 |     }
285 |   }
286 | }
287 | 
```

--------------------------------------------------------------------------------
/src/codebase.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as fs from 'fs';
  2 | import * as path from 'path';
  3 | import { exec, execSync } from 'child_process';
  4 | import { promisify } from 'util';
  5 | 
  6 | const execAsync = promisify(exec);
  7 | 
  8 | export interface CodebaseAnalysisResult {
  9 |   repoName: string;
 10 |   repoDescription?: string;
 11 |   languages: {[key: string]: number};
 12 |   fileCount: number;
 13 |   recentCommits: Array<{
 14 |     hash: string;
 15 |     author: string;
 16 |     date: string;
 17 |     message: string;
 18 |   }>;
 19 |   technologies: string[];
 20 |   summary: string;
 21 |   readmeContent?: string;
 22 | }
 23 | 
 24 | export class CodebaseAnalyzer {
 25 |   private rootDir: string;
 26 |   
 27 |   constructor(rootDir?: string) {
 28 |     // If no root directory is provided, use the current working directory
 29 |     this.rootDir = rootDir || process.cwd();
 30 |   }
 31 |   
 32 |   /**
 33 |    * Get the repository name from the remote URL
 34 |    */
 35 |   async getRepoDetails(): Promise<{name: string; owner: string; description?: string}> {
 36 |     try {
 37 |       // Get the remote URL
 38 |       const { stdout: remoteUrl } = await execAsync('git config --get remote.origin.url', { cwd: this.rootDir });
 39 |       
 40 |       // Parse the remote URL to get the owner and repo name
 41 |       const match = remoteUrl.trim().match(/github\.com[\/:]([^\/]+)\/([^\/\.]+)(?:\.git)?$/);
 42 |       
 43 |       if (!match) {
 44 |         return {
 45 |           name: path.basename(this.rootDir),
 46 |           owner: 'unknown'
 47 |         };
 48 |       }
 49 |       
 50 |       const owner = match[1];
 51 |       const name = match[2];
 52 |       
 53 |       // Try to get repository description from package.json if available
 54 |       let description: string | undefined;
 55 |       try {
 56 |         const packageJsonPath = path.join(this.rootDir, 'package.json');
 57 |         if (fs.existsSync(packageJsonPath)) {
 58 |           const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
 59 |           description = packageJson.description;
 60 |         }
 61 |       } catch (error) {
 62 |         console.log('Error reading package.json:', error);
 63 |       }
 64 |       
 65 |       return { name, owner, description };
 66 |     } catch (error) {
 67 |       console.log('Error getting repo details:', error);
 68 |       // If we can't get the repo details from git, use the directory name
 69 |       return {
 70 |         name: path.basename(this.rootDir),
 71 |         owner: 'unknown'
 72 |       };
 73 |     }
 74 |   }
 75 |   
 76 |   /**
 77 |    * Count the number of files by extension
 78 |    */
 79 |   async countFilesByLanguage(): Promise<{[key: string]: number}> {
 80 |     const languages: {[key: string]: number} = {};
 81 |     
 82 |     try {
 83 |       // Use git ls-files to get all tracked files
 84 |       const { stdout } = await execAsync('git ls-files', { cwd: this.rootDir });
 85 |       const files = stdout.split('\n').filter(Boolean);
 86 |       
 87 |       for (const file of files) {
 88 |         const ext = path.extname(file).toLowerCase();
 89 |         if (ext) {
 90 |           // Remove the leading dot from the extension
 91 |           const language = ext.substring(1);
 92 |           languages[language] = (languages[language] || 0) + 1;
 93 |         }
 94 |       }
 95 |     } catch (error) {
 96 |       console.log('Error counting files by language:', error);
 97 |       // Fallback to a simple directory walk if git command fails
 98 |       this.walkDirectory(this.rootDir, languages);
 99 |     }
100 |     
101 |     return languages;
102 |   }
103 |   
104 |   /**
105 |    * Walk a directory recursively to count files by extension
106 |    */
107 |   private walkDirectory(dir: string, languages: {[key: string]: number}): void {
108 |     try {
109 |       const files = fs.readdirSync(dir);
110 |       
111 |       for (const file of files) {
112 |         const filePath = path.join(dir, file);
113 |         const stat = fs.statSync(filePath);
114 |         
115 |         if (stat.isDirectory() && !file.startsWith('.') && file !== 'node_modules') {
116 |           this.walkDirectory(filePath, languages);
117 |         } else if (stat.isFile()) {
118 |           const ext = path.extname(file).toLowerCase();
119 |           if (ext) {
120 |             // Remove the leading dot from the extension
121 |             const language = ext.substring(1);
122 |             languages[language] = (languages[language] || 0) + 1;
123 |           }
124 |         }
125 |       }
126 |     } catch (error) {
127 |       console.log('Error walking directory:', error);
128 |     }
129 |   }
130 |   
131 |   /**
132 |    * Get recent commits in the repository
133 |    */
134 |   async getRecentCommits(count: number = 20): Promise<Array<{hash: string; author: string; date: string; message: string}>> {
135 |     try {
136 |       const { stdout } = await execAsync(
137 |         `git log -n ${count} --pretty=format:"%H|%an|%ad|%s"`,
138 |         { cwd: this.rootDir }
139 |       );
140 |       
141 |       return stdout.split('\n')
142 |         .filter(Boolean)
143 |         .map(line => {
144 |           const [hash, author, date, ...messageParts] = line.split('|');
145 |           return {
146 |             hash,
147 |             author,
148 |             date,
149 |             message: messageParts.join('|') // In case the message itself contains the delimiter
150 |           };
151 |         });
152 |     } catch (error) {
153 |       console.log('Error getting recent commits:', error);
154 |       return [];
155 |     }
156 |   }
157 |   
158 |   /**
159 |    * Detect technologies used in the project
160 |    */
161 |   async detectTechnologies(): Promise<string[]> {
162 |     const technologies: Set<string> = new Set();
163 |     
164 |     try {
165 |       // Check for common configuration files
166 |       const files = await fs.promises.readdir(this.rootDir);
167 |       
168 |       // Framework detection
169 |       if (files.includes('package.json')) {
170 |         technologies.add('Node.js');
171 |         
172 |         // Read package.json to detect more technologies
173 |         const packageJson = JSON.parse(await fs.promises.readFile(path.join(this.rootDir, 'package.json'), 'utf-8'));
174 |         const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
175 |         
176 |         if (deps.react) technologies.add('React');
177 |         if (deps.vue) technologies.add('Vue.js');
178 |         if (deps.angular || deps['@angular/core']) technologies.add('Angular');
179 |         if (deps.express) technologies.add('Express.js');
180 |         if (deps.next) technologies.add('Next.js');
181 |         if (deps.gatsby) technologies.add('Gatsby');
182 |         if (deps.electron) technologies.add('Electron');
183 |         if (deps.typescript) technologies.add('TypeScript');
184 |         if (deps.webpack) technologies.add('Webpack');
185 |         if (deps.jest || deps.mocha || deps.jasmine) technologies.add('Testing');
186 |         if (deps.tailwindcss) technologies.add('Tailwind CSS');
187 |         if (deps.bootstrap) technologies.add('Bootstrap');
188 |         if (deps.eslint) technologies.add('ESLint');
189 |         if (deps.prettier) technologies.add('Prettier');
190 |         if (deps.prisma) technologies.add('Prisma');
191 |         if (deps['@prisma/client']) technologies.add('Prisma');
192 |         if (deps.sequelize) technologies.add('Sequelize');
193 |         if (deps.mongoose) technologies.add('MongoDB');
194 |         if (deps.redis) technologies.add('Redis');
195 |         if (deps.graphql) technologies.add('GraphQL');
196 |         if (deps.apollo) technologies.add('Apollo');
197 |         if (deps['@supabase/supabase-js']) technologies.add('Supabase');
198 |       }
199 |       
200 |       if (files.includes('go.mod')) technologies.add('Go');
201 |       if (files.includes('Cargo.toml')) technologies.add('Rust');
202 |       if (files.includes('requirements.txt') || files.includes('setup.py')) technologies.add('Python');
203 |       if (files.includes('composer.json')) technologies.add('PHP');
204 |       if (files.includes('Gemfile')) technologies.add('Ruby');
205 |       if (files.includes('pom.xml') || files.includes('build.gradle')) technologies.add('Java');
206 |       if (files.includes('Dockerfile')) technologies.add('Docker');
207 |       if (files.includes('.github')) technologies.add('GitHub Actions');
208 |       if (files.includes('.gitlab-ci.yml')) technologies.add('GitLab CI');
209 |       if (files.includes('serverless.yml')) technologies.add('Serverless Framework');
210 |       if (files.includes('terraform')) technologies.add('Terraform');
211 |       
212 |       // Database files
213 |       if (files.includes('prisma')) technologies.add('Prisma');
214 |       if (files.some(f => f.includes('migration'))) technologies.add('Database Migrations');
215 |       
216 |     } catch (error) {
217 |       console.log('Error detecting technologies:', error);
218 |     }
219 |     
220 |     return Array.from(technologies);
221 |   }
222 |   
223 |   /**
224 |    * Count total number of files in the repository
225 |    */
226 |   async countFiles(): Promise<number> {
227 |     try {
228 |       const { stdout } = await execAsync('git ls-files | wc -l', { cwd: this.rootDir });
229 |       return parseInt(stdout.trim(), 10);
230 |     } catch (error) {
231 |       console.log('Error counting files:', error);
232 |       return 0;
233 |     }
234 |   }
235 |   
236 |   /**
237 |    * Read README.md content if it exists
238 |    */
239 |   async getReadmeContent(): Promise<string | undefined> {
240 |     try {
241 |       // Look for README.md (case insensitive)
242 |       const files = await fs.promises.readdir(this.rootDir);
243 |       const readmeFile = files.find(file => 
244 |         file.toLowerCase() === 'readme.md' || file.toLowerCase() === 'readme.markdown'
245 |       );
246 |       
247 |       if (readmeFile) {
248 |         const readmePath = path.join(this.rootDir, readmeFile);
249 |         const content = await fs.promises.readFile(readmePath, 'utf-8');
250 |         return content;
251 |       }
252 |       
253 |       return undefined;
254 |     } catch (error) {
255 |       console.log('Error reading README.md:', error);
256 |       return undefined;
257 |     }
258 |   }
259 |   
260 |   /**
261 |    * Analyze the codebase and collect information
262 |    */
263 |   async analyze(): Promise<CodebaseAnalysisResult> {
264 |     const repoDetails = await this.getRepoDetails();
265 |     
266 |     const [
267 |       languages,
268 |       recentCommits,
269 |       technologies,
270 |       fileCount,
271 |       readmeContent
272 |     ] = await Promise.all([
273 |       this.countFilesByLanguage(),
274 |       this.getRecentCommits(),
275 |       this.detectTechnologies(),
276 |       this.countFiles(),
277 |       this.getReadmeContent()
278 |     ]);
279 |     
280 |     // Generate a summary
281 |     const topLanguages = Object.entries(languages)
282 |       .sort((a, b) => b[1] - a[1])
283 |       .slice(0, 3)
284 |       .map(([lang]) => lang);
285 |       
286 |     const summary = `${repoDetails.name} is a ${topLanguages.join('/')} project with ${fileCount} files using ${technologies.slice(0, 5).join(', ')}.`;
287 |     
288 |     return {
289 |       repoName: repoDetails.name,
290 |       repoDescription: repoDetails.description,
291 |       languages,
292 |       fileCount,
293 |       recentCommits,
294 |       technologies,
295 |       summary,
296 |       readmeContent
297 |     };
298 |   }
299 | }
300 | 
```

--------------------------------------------------------------------------------
/src/openai.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import OpenAI from "openai";
  2 | import { Resume } from "./types.js";
  3 | import { CodebaseAnalysisResult } from "./codebase.js";
  4 | import { resumeUpdateSchema, ResumeUpdate } from "./schemas.js";
  5 | import { z } from "zod";
  6 | 
  7 | export class OpenAIService {
  8 |   private client: OpenAI;
  9 | 
 10 |   constructor(apiKey: string) {
 11 |     if (!apiKey) {
 12 |       throw new Error("OpenAI API key is required");
 13 |     }
 14 |     this.client = new OpenAI({ apiKey });
 15 |   }
 16 | 
 17 |   /**
 18 |    * Generate a new project and skills based on codebase analysis
 19 |    */
 20 |   async generateResumeEnhancement(
 21 |     codebaseAnalysis: CodebaseAnalysisResult
 22 |   ): Promise<ResumeUpdate> {
 23 |     try {
 24 |       console.log("Preparing OpenAI API call for resume enhancement...");
 25 |       
 26 |       // Call OpenAI API with function calling
 27 |       const response = await this.client.chat.completions.create({
 28 |         model: "gpt-4",
 29 |         messages: [
 30 |           {
 31 |             role: "system",
 32 |             content:
 33 |               "You are a professional technical resume writer that creates high-quality JSON Resume compatible project entries and skills based on codebase analysis. Focus on capturing the essence and significance of the project, not just technical details.\n\n" +
 34 |               "Guidelines:\n" +
 35 |               "- Begin by clearly explaining what the project DOES and its PURPOSE - this is the most important part\n" +
 36 |               "- Focus on the problem it solves and value it provides to users or stakeholders\n" +
 37 |               "- Then mention key technical achievements and architectural decisions\n" +
 38 |               "- Create professional, substantive descriptions that highlight business value\n" +
 39 |               "- Avoid trivial details like file counts or minor technologies\n" +
 40 |               "- For dates, use YYYY-MM-DD format and ensure they are realistic (not in the future)\n" +
 41 |               "- For ongoing projects, omit the endDate field entirely\n" +
 42 |               "- Only include skills that are substantive and resume-worthy\n" +
 43 |               "- Group skills by category when possible\n" +
 44 |               "- Prioritize quality over quantity in skills and descriptions",
 45 |           },
 46 |           { 
 47 |             role: "user", 
 48 |             content: `Based on this codebase analysis, generate a single project entry and relevant skills for a resume that focuses first on WHAT the project does and WHY it matters, then how it was implemented:
 49 | 
 50 | ${codebaseAnalysis.readmeContent ? `README.md Content:\n${codebaseAnalysis.readmeContent}\n\n` : ''}
 51 | 
 52 | Codebase Analysis:\n${JSON.stringify(codebaseAnalysis, null, 2)}` 
 53 |           },
 54 |         ],
 55 |         functions: [
 56 |           {
 57 |             name: "create_resume_update",
 58 |             description: "Create a new project entry and skills based on codebase analysis",
 59 |             parameters: {
 60 |               type: "object",
 61 |               properties: {
 62 |                 newProject: {
 63 |                   type: "object",
 64 |                   properties: {
 65 |                     name: { 
 66 |                       type: "string",
 67 |                       description: "Professional project name"
 68 |                     },
 69 |                     startDate: { 
 70 |                       type: "string", 
 71 |                       description: "Project start date in YYYY-MM-DD or YYYY-MM format (must be a realistic date, not in the future)"
 72 |                     },
 73 |                     endDate: { 
 74 |                       type: "string", 
 75 |                       description: "Project end date in YYYY-MM-DD or YYYY-MM format. OMIT THIS FIELD ENTIRELY for ongoing projects - do not use 'Present' or future dates."
 76 |                     },
 77 |                     description: { 
 78 |                       type: "string",
 79 |                       description: "Professional project description that STARTS by clearly explaining what the project does and why it matters. Begin with its purpose and function, then mention key technologies and implementation details. (60-100 words recommended)"
 80 |                     },
 81 |                     highlights: { 
 82 |                       type: "array",
 83 |                       items: { type: "string" },
 84 |                       description: "Bullet points highlighting key achievements and technologies that demonstrate significant impact"
 85 |                     },
 86 |                     url: { 
 87 |                       type: "string",
 88 |                       description: "Project URL" 
 89 |                     },
 90 |                     roles: { 
 91 |                       type: "array", 
 92 |                       items: { type: "string" },
 93 |                       description: "Roles held during the project"
 94 |                     },
 95 |                     entity: { 
 96 |                       type: "string",
 97 |                       description: "Organization name associated with the project"
 98 |                     },
 99 |                     type: { 
100 |                       type: "string",
101 |                       description: "Type of project (application, library, etc.)"
102 |                     }
103 |                   },
104 |                   required: ["name", "startDate", "description"]
105 |                 },
106 |                 newSkills: {
107 |                   type: "array",
108 |                   description: "Only include substantive, resume-worthy skills that demonstrate significant expertise",
109 |                   items: {
110 |                     type: "object",
111 |                     properties: {
112 |                       name: { 
113 |                         type: "string",
114 |                         description: "Professional skill name"
115 |                       },
116 |                       level: { 
117 |                         type: "string",
118 |                         description: "Skill proficiency level"
119 |                       },
120 |                       keywords: { 
121 |                         type: "array",
122 |                         items: { type: "string" },
123 |                         description: "Related keywords for this skill"
124 |                       },
125 |                       category: {
126 |                         type: "string",
127 |                         description: "Skill category for grouping (e.g., 'Programming Languages', 'Frameworks', 'Tools')"
128 |                       }
129 |                     },
130 |                     required: ["name"]
131 |                   }
132 |                 },
133 |                 changes: {
134 |                   type: "array",
135 |                   items: { type: "string" },
136 |                   description: "Summary of changes made to the resume"
137 |                 }
138 |               },
139 |               required: ["newProject", "newSkills", "changes"]
140 |             }
141 |           }
142 |         ],
143 |         function_call: { name: "create_resume_update" }
144 |       });
145 | 
146 |       console.log("Received response from OpenAI API");
147 |       
148 |       const functionCall = response.choices[0]?.message?.function_call;
149 |       if (!functionCall?.arguments) {
150 |         console.log("Error: No function call arguments in OpenAI response");
151 |         throw new Error("No function call arguments received from OpenAI");
152 |       }
153 | 
154 |       // Parse and validate the response
155 |       console.log("Parsing and validating OpenAI response...");
156 |       try {
157 |         const result = JSON.parse(functionCall.arguments);
158 |         const validated = resumeUpdateSchema.parse(result);
159 |         console.log("Successfully validated schema");
160 |         return validated;
161 |       } catch (parseError) {
162 |         console.log("Error parsing OpenAI response:", 
163 |           parseError instanceof SyntaxError ? "JSON parse error" : 
164 |           parseError instanceof z.ZodError ? "Schema validation error" : 
165 |           "Unknown error"
166 |         );
167 |         console.log("Raw function call arguments:", functionCall.arguments.substring(0, 200) + "...");
168 |         throw parseError;
169 |       }
170 |     } catch (error) {
171 |       if (error instanceof z.ZodError) {
172 |         console.log("Schema validation error:", error.errors);
173 |       } else if (error instanceof SyntaxError) {
174 |         console.log("JSON parsing error:", error.message);
175 |       } else if (error instanceof Error) {
176 |         console.log("OpenAI API error:", error.message);
177 |         console.log("Error details:", error);
178 |       } else {
179 |         console.log("Unknown error:", error);
180 |       }
181 |       throw error;
182 |     }
183 |   }
184 | 
185 |   /**
186 |    * Add only new project and skills to a resume without modifying any existing content
187 |    */
188 |   async enhanceResume(resume: Resume, update: ResumeUpdate): Promise<Resume> {
189 |     const result = JSON.parse(JSON.stringify(resume)) as Resume;
190 |     
191 |     // Store the _gistId separately so we can add it back later
192 |     const gistId = (result as any)._gistId;
193 |     
194 |     // Add the new project if it doesn't already exist
195 |     const existingProjects = new Set(
196 |       (result.projects || []).map(project => project.name.toLowerCase())
197 |     );
198 |     
199 |     if (update.newProject && !existingProjects.has(update.newProject.name.toLowerCase())) {
200 |       result.projects = [...(result.projects || []), update.newProject];
201 |     }
202 |     
203 |     // Add new skills if they don't already exist
204 |     const existingSkills = new Set(
205 |       (result.skills || []).map(skill => 
206 |         typeof skill === 'string' ? skill.toLowerCase() : skill.name.toLowerCase()
207 |       )
208 |     );
209 |     
210 |     const newSkills = update.newSkills.filter(skill => 
211 |       !existingSkills.has(skill.name.toLowerCase())
212 |     );
213 |     
214 |     if (newSkills.length > 0) {
215 |       result.skills = [...(result.skills || []), ...newSkills];
216 |     }
217 |     
218 |     // Add back the _gistId if it existed
219 |     if (gistId) {
220 |       (result as any)._gistId = gistId;
221 |     }
222 |     
223 |     return result;
224 |   }
225 | 
226 |   /**
227 |    * Generate a summary of updates made to the resume
228 |    */
229 |   async generateUpdateSummary(changes: string[]): Promise<string> {
230 |     try {
231 |       console.log("Generating update summary...");
232 |       
233 |       // Call OpenAI API to generate a summary
234 |       const response = await this.client.chat.completions.create({
235 |         model: "gpt-3.5-turbo",
236 |         messages: [
237 |           {
238 |             role: "system",
239 |             content: "You are a helpful assistant that creates concise summaries of resume updates."
240 |           },
241 |           {
242 |             role: "user",
243 |             content: `Create a brief, professional summary of these changes made to a resume:\n${changes.join('\n')}`
244 |           }
245 |         ],
246 |         max_tokens: 150
247 |       });
248 |       
249 |       const summary = response.choices[0]?.message?.content || "Resume updated with new project details and skills.";
250 |       return summary;
251 |     } catch (error) {
252 |       console.log("Error generating update summary:", error);
253 |       return "Resume updated with new project details and skills.";
254 |     }
255 |   }
256 | }
257 | 
```

--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { config } from "dotenv";
  2 | import { Server } from "../typescript-sdk/src/server/index.js";
  3 | import { StdioServerTransport } from "../typescript-sdk/src/server/stdio.js";
  4 | import { SSEServerTransport } from "../typescript-sdk/src/server/sse.js";
  5 | import {
  6 |   CallToolRequestSchema,
  7 |   ErrorCode,
  8 |   ListToolsRequestSchema,
  9 |   McpError,
 10 |   Tool,
 11 | } from "../typescript-sdk/src/types.js";
 12 | import { GitHubService } from "./src/github.js";
 13 | import { OpenAIService } from "./src/openai.js";
 14 | import { Resume } from "./src/types.js";
 15 | import { CodebaseAnalyzer } from "./src/codebase.js";
 16 | import { ResumeEnhancer } from "./src/resume-enhancer.js";
 17 | import { tools, ANALYZE_CODEBASE_TOOL, CHECK_RESUME_TOOL, ENHANCE_RESUME_WITH_PROJECT_TOOL } from "./src/tools.js";
 18 | import { Hono } from "hono";
 19 | import { serve } from "@hono/node-server";
 20 | 
 21 | // Load environment variables from .env file
 22 | config();
 23 | 
 24 | const server = new Server(
 25 |   {
 26 |     name: "jsonresume-mcp",
 27 |     version: "1.0.0",
 28 |   },
 29 |   {
 30 |     capabilities: {
 31 |       resources: {},
 32 |       tools: {},
 33 |       logging: {},
 34 |     },
 35 |   }
 36 | );
 37 | 
 38 | // Environment variables (loaded from .env file via dotenv)
 39 | const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
 40 | const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
 41 | const GITHUB_USERNAME = process.env.GITHUB_USERNAME;
 42 | 
 43 | // Initialize services
 44 | let githubService: GitHubService;
 45 | let openaiService: OpenAIService;
 46 | let codebaseAnalyzer: CodebaseAnalyzer;
 47 | let resumeEnhancer: ResumeEnhancer;
 48 | 
 49 | try {
 50 |   if (!GITHUB_TOKEN) {
 51 |     throw new Error("GITHUB_TOKEN environment variable is required");
 52 |   }
 53 |   if (!OPENAI_API_KEY) {
 54 |     throw new Error("OPENAI_API_KEY environment variable is required");
 55 |   }
 56 |   if (!GITHUB_USERNAME) {
 57 |     throw new Error("GITHUB_USERNAME environment variable is required");
 58 |   }
 59 | 
 60 |   githubService = new GitHubService(GITHUB_TOKEN, GITHUB_USERNAME);
 61 |   openaiService = new OpenAIService(OPENAI_API_KEY);
 62 |   codebaseAnalyzer = new CodebaseAnalyzer(process.cwd());
 63 |   resumeEnhancer = new ResumeEnhancer(openaiService);
 64 |   
 65 |   console.log("Services initialized successfully");
 66 | } catch (error) {
 67 |   console.log("Error initializing services:", error);
 68 |   process.exit(1);
 69 | }
 70 | 
 71 | 
 72 | 
 73 | server.setRequestHandler(ListToolsRequestSchema, async () => ({
 74 |   tools,
 75 | }));
 76 | 
 77 | function doHello(name: string) {
 78 |   return {
 79 |     message: `Hello, ${name}!`,
 80 |   };
 81 | }
 82 | 
 83 | async function analyzeCodebase(directory?: string) {
 84 |   try {
 85 |     console.log("Starting codebase analysis...");
 86 |     
 87 |     // Create a new analyzer for the specified directory
 88 |     const analyzer = directory ? new CodebaseAnalyzer(directory) : codebaseAnalyzer;
 89 |     
 90 |     // Analyze the codebase
 91 |     const analysis = await analyzer.analyze();
 92 |     
 93 |     console.log("Codebase analysis completed");
 94 |     
 95 |     return {
 96 |       message: "Codebase analysis completed successfully",
 97 |       analysis,
 98 |       summary: analysis.summary
 99 |     };
100 |   } catch (error) {
101 |     console.log("Error analyzing codebase:", error);
102 |     throw error;
103 |   }
104 | }
105 | 
106 | async function checkResume() {
107 |   try {
108 |     console.log("Checking for existing resume...");
109 |     
110 |     // Fetch the user's resume from GitHub gists
111 |     const resume = await githubService.getResumeFromGists();
112 |     
113 |     if (!resume) {
114 |       return {
115 |         message: "No resume found",
116 |         exists: false,
117 |         resumeUrl: null
118 |       };
119 |     }
120 |     
121 |     // Remove the _gistId property for cleaner output
122 |     const { _gistId, ...cleanResume } = resume;
123 |     
124 |     return {
125 |       message: "Resume found",
126 |       exists: true,
127 |       resumeUrl: `https://registry.jsonresume.org/${GITHUB_USERNAME}`,
128 |       resume: cleanResume
129 |     };
130 |   } catch (error) {
131 |     console.log("Error checking resume:", error);
132 |     throw error;
133 |   }
134 | }
135 | 
136 | async function enhanceResumeWithProject(directory?: string) {
137 |   try {
138 |     console.log("Starting resume enhancement with current project...");
139 |     
140 |     // Step 1: Fetch the user's resume from GitHub gists
141 |     console.log("Fetching resume from GitHub gists...");
142 |     let resume = await githubService.getResumeFromGists();
143 |     
144 |     if (!resume) {
145 |       // If no resume exists, create a sample one
146 |       console.log("No resume found, creating a sample resume...");
147 |       const userProfile = await githubService.getUserProfile();
148 |       resume = await githubService.createSampleResume();
149 |       console.log("Sample resume created successfully");
150 |     } else {
151 |       console.log("Existing resume found");
152 |     }
153 |     
154 |     // Step 2: Analyze the current codebase
155 |     console.log("Analyzing current project...");
156 |     const analyzer = directory ? new CodebaseAnalyzer(directory) : codebaseAnalyzer;
157 |     const codebaseAnalysis = await analyzer.analyze();
158 |     
159 |     // Step 3: Enhance the resume with the current project
160 |     console.log("Enhancing resume with current project...");
161 |     const { updatedResume, changes, summary, userMessage, resumeLink } = await resumeEnhancer.enhanceWithCurrentProject(
162 |       resume,
163 |       codebaseAnalysis,
164 |       GITHUB_USERNAME || ''
165 |     );
166 |     
167 |     // Step 4: Update the resume on GitHub
168 |     console.log("Updating resume on GitHub...");
169 |     const finalResume = await githubService.updateResume(updatedResume);
170 |     
171 |     return {
172 |       message: "Resume enhanced with current project successfully",
173 |       changes: changes,
174 |       summary,
175 |       userMessage,
176 |       resumeUrl: resumeLink || `https://registry.jsonresume.org/${GITHUB_USERNAME}`,
177 |       projectName: codebaseAnalysis.repoName,
178 |       warning: "⚠️ Note: Automatic resume updates might have modified your resume in ways that don't match your preferences. You can revert to a previous version through your GitHub Gist revision history if needed."
179 |     };
180 |   } catch (error) {
181 |     console.log("Error enhancing resume with project:", error);
182 |     throw error;
183 |   }
184 | }
185 | 
186 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
187 |   try {
188 |     console.log(`[MCP] Tool call: ${request.params.name}`, request.params.arguments);
189 |     
190 |     // Validate the tool name
191 |     if (!tools.some(tool => tool.name === request.params.name)) {
192 |       return {
193 |         isError: true,
194 |         content: [{
195 |           type: "text",
196 |           text: `Error: Unknown tool '${request.params.name}'`
197 |         }]
198 |       };
199 |     }
200 |     
201 |     // Execute the appropriate tool
202 |     if (request.params.name === "github_hello_tool") {
203 |       const input = request.params.arguments as { name: string };
204 |       const result = doHello(input.name);
205 |       
206 |       return {
207 |         content: [{
208 |           type: "text",
209 |           text: result.message
210 |         }]
211 |       };
212 |     } else if (request.params.name === ANALYZE_CODEBASE_TOOL.name) {
213 |       const input = request.params.arguments as { directory?: string };
214 |       const result = await analyzeCodebase(input.directory);
215 |       
216 |       return {
217 |         content: [{
218 |           type: "text",
219 |           text: JSON.stringify(result, null, 2)
220 |         }]
221 |       };
222 |     } else if (request.params.name === CHECK_RESUME_TOOL.name) {
223 |       const result = await checkResume();
224 |       
225 |       return {
226 |         content: [{
227 |           type: "text",
228 |           text: JSON.stringify(result, null, 2)
229 |         }]
230 |       };
231 |     } else if (request.params.name === ENHANCE_RESUME_WITH_PROJECT_TOOL.name) {
232 |       const input = request.params.arguments as { directory?: string };
233 |       const result = await enhanceResumeWithProject(input.directory);
234 |       
235 |       return {
236 |         content: [{
237 |           type: "text",
238 |           text: JSON.stringify(result, null, 2)
239 |         }]
240 |       };
241 |     }
242 |     
243 |     // This should never happen due to our validation above
244 |     return {
245 |       isError: true,
246 |       content: [{
247 |         type: "text",
248 |         text: `Error: Tool '${request.params.name}' implementation not found`
249 |       }]
250 |     };
251 |   } catch (error) {
252 |     console.error(`[MCP] Error executing tool ${request.params.name}:`, error);
253 |     
254 |     // Return a proper error response
255 |     return {
256 |       isError: true,
257 |       content: [{
258 |         type: "text",
259 |         text: `Error executing tool: ${error.message || String(error)}`
260 |       }]
261 |     };
262 |   }
263 | });
264 | 
265 | server.onerror = (error: any) => {
266 |   console.log(error);
267 | };
268 | 
269 | process.on("SIGINT", async () => {
270 |   await server.close();
271 |   process.exit(0);
272 | });
273 | 
274 | async function runStdioServer() {
275 |   const transport = new StdioServerTransport();
276 |   await server.connect(transport);
277 |   console.log("JsonResume MCP Server running on stdio");
278 | }
279 | 
280 | async function runHttpServer() {
281 |   const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3000;
282 |   const app = new Hono();
283 |   
284 |   app.get('/', (c) => {
285 |     return c.json({
286 |       message: 'Hello, I\'m JSON Resume MCP Server',
287 |       description: 'This is a ModelContextProtocol server for enhancing JSON Resumes',
288 |       usage: {
289 |         http: 'npx -y @jsonresume/mcp',
290 |         stdio: 'npx -y @jsonresume/mcp stdio'
291 |       },
292 |       version: '3.0.3'
293 |     });
294 |   });
295 |   
296 |   // Add MCP message endpoint for client->server communication
297 |   app.post('/message', async (c) => {
298 |     const sessionId = c.req.query('sessionId');
299 |     if (!sessionId) {
300 |       return c.json({ error: 'No session ID provided' }, 400);
301 |     }
302 | 
303 |     try {
304 |       const body = await c.req.json();
305 |       console.log(`[MCP] Received message for session ${sessionId}:`, body);
306 |       
307 |       // Find the transport for this session and handle the message
308 |       const transport = activeTransports.get(sessionId);
309 |       if (!transport) {
310 |         return c.json({ error: 'Invalid session ID' }, 404);
311 |       }
312 |       
313 |       // Special handling for initialize message
314 |       if (body.method === 'initialize') {
315 |         console.log(`[MCP] Handling initialize message for session ${sessionId}`);
316 |         // Send initialize response directly via SSE
317 |         const response = {
318 |           jsonrpc: "2.0",
319 |           id: body.id,
320 |           result: {
321 |             serverInfo: {
322 |               name: "jsonresume-mcp",
323 |               version: "1.0.0",
324 |             },
325 |             capabilities: {
326 |               tools: {},
327 |               resources: {},
328 |               logging: {}
329 |             }
330 |           }
331 |         };
332 |         
333 |         // Let's directly use the controller we stored when setting up the SSE connection
334 |         console.log(`[MCP] Sending initialize response to session ${sessionId}`);
335 |         
336 |         // Store controllers along with transports
337 |         const sseData = `data: ${JSON.stringify(response)}\n\n`;
338 |         
339 |         // Directly write to the response stream
340 |         // This bypasses the transport's send method
341 |         const res = transport['res'];
342 |         if (res && typeof res.write === 'function') {
343 |           try {
344 |             res.write(sseData);
345 |             console.log(`[MCP] Successfully wrote initialize response to stream`);
346 |           } catch (error) {
347 |             console.error(`[MCP] Error writing to stream:`, error);
348 |           }
349 |         } else {
350 |           console.error(`[MCP] Could not access response object for session ${sessionId}`);
351 |         }
352 |         
353 |         return c.json({ status: 'ok' });
354 |       }
355 |       
356 |       await transport.handlePostMessage(c.req.raw, new Response() as any, body);
357 |       return c.json({ status: 'ok' });
358 |     } catch (error) {
359 |       console.error(`[MCP] Error handling message:`, error);
360 |       return c.json({ error: String(error) }, 500);
361 |     }
362 |   });
363 | 
364 |   // Store active SSE transports
365 |   const activeTransports = new Map<string, SSEServerTransport>();
366 | 
367 |   // Add SSE endpoint for server->client streaming
368 |   app.get('/sse', async (c) => {
369 |     // Set up SSE headers
370 |     c.header('Content-Type', 'text/event-stream');
371 |     c.header('Cache-Control', 'no-cache');
372 |     c.header('Connection', 'keep-alive');
373 |     
374 |     // Create a wrapper for the Hono response object to mimic Node's ServerResponse
375 |     const honoResponseAdapter = {
376 |       _data: [] as string[],
377 |       headersSent: false,
378 |       writeHead: function(statusCode: number, headers?: Record<string, string>) {
379 |         console.log(`[Adapter] writeHead called with status ${statusCode}`);
380 |         this.headersSent = true;
381 |         return this;
382 |       },
383 |       write: function(chunk: string) {
384 |         console.log(`[Adapter] write called with chunk length ${chunk.length}`);
385 |         this._data.push(chunk);
386 |         return true;
387 |       },
388 |       end: function() {
389 |         console.log(`[Adapter] end called`);
390 |         return this;
391 |       },
392 |       _listeners: {} as Record<string, Function[]>,
393 |       on: function(event: string, listener: Function) {
394 |         console.log(`[Adapter] Adding listener for ${event}`);
395 |         if (!this._listeners[event]) {
396 |           this._listeners[event] = [];
397 |         }
398 |         this._listeners[event].push(listener);
399 |         return this;
400 |       },
401 |     };
402 |     
403 |     // Create transport with message endpoint as the target for client->server messages
404 |     const transport = new SSEServerTransport("/message", honoResponseAdapter as unknown as ServerResponse);
405 |     let sessionId: string;
406 | 
407 |     // Keep the connection alive
408 |     return new Response(
409 |       new ReadableStream({
410 |         start(controller) {
411 |           console.log("[SSE] Stream started");
412 |           
413 |           // Add adapter method to forward data from the adapter to the stream
414 |           const adapter = transport['res'] as any;
415 |           const originalWrite = adapter.write;
416 |           adapter.write = function(chunk: string) {
417 |             console.log(`[Stream Relay] Received chunk of length ${chunk.length}, forwarding to client`);
418 |             controller.enqueue(chunk);
419 |             return originalWrite.call(this, chunk);
420 |           };
421 | 
422 |           // Set up transport event handlers
423 |           transport.onmessage = (message) => {
424 |             console.log(`[SSE] Server message:`, message);
425 |             controller.enqueue(`data: ${JSON.stringify(message)}\n\n`);
426 |           };
427 |           
428 |           transport.onerror = (error) => {
429 |             console.error(`[SSE] Transport error:`, error);
430 |             controller.error(error);
431 |           };
432 | 
433 |           // Connect transport to server and store it
434 |           server.connect(transport).then(() => {
435 |             sessionId = transport.sessionId;
436 |             activeTransports.set(sessionId, transport);
437 |             
438 |             // Send endpoint event with session ID
439 |             const endpointUrl = `/message?sessionId=${sessionId}`;
440 |             controller.enqueue(`event: endpoint\ndata: ${endpointUrl}\n\n`);
441 |             
442 |             // Send initial connected event
443 |             controller.enqueue('event: connected\ndata: {"status":"connected"}\n\n');
444 |           }).catch(error => {
445 |             console.error(`[SSE] Connection error:`, error);
446 |             controller.error(error);
447 |           });
448 |           
449 |           // Set up keep-alive ping
450 |           const pingInterval = setInterval(() => {
451 |             try {
452 |               console.log(`[SSE] Sending ping`);
453 |               controller.enqueue(`event: ping\ndata: ${Date.now()}\n\n`);
454 |             } catch (err) {
455 |               console.error(`[SSE] Ping error:`, err);
456 |               clearInterval(pingInterval);
457 |             }
458 |           }, 15000);
459 |           
460 |           // Handle client disconnect
461 |           c.req.raw.signal.addEventListener('abort', () => {
462 |             console.log(`[SSE] Client disconnected, cleaning up session ${sessionId}`);
463 |             clearInterval(pingInterval);
464 |             if (sessionId) {
465 |               // Just remove the transport from our map
466 |               activeTransports.delete(sessionId);
467 |               // Don't call server.disconnect() as it's not a function on the server instance
468 |               // Instead, we'll just clean up the transport
469 |               try {
470 |                 // Notify any listeners that might be attached to the transport
471 |                 if (transport.onerror) {
472 |                   transport.onerror(new Error('Client disconnected'));
473 |                 }
474 |               } catch (err) {
475 |                 console.error(`[SSE] Error during cleanup:`, err);
476 |               }
477 |             }
478 |           });
479 |         }
480 |       }),
481 |       {
482 |         headers: {
483 |           'Content-Type': 'text/event-stream',
484 |           'Cache-Control': 'no-cache',
485 |           'Connection': 'keep-alive'
486 |         }
487 |       }
488 |     );
489 |   });
490 |   
491 |   console.log(`JsonResume MCP Server starting on port ${PORT}...`);
492 |   serve({
493 |     fetch: app.fetch,
494 |     port: Number(PORT)
495 |   });
496 |   
497 |   console.log(`JsonResume MCP Server running at http://localhost:${PORT}`);
498 |   console.log(`SSE endpoint available at http://localhost:${PORT}/sse`);
499 |   console.log(`To use in stdio mode, run: npx -y @jsonresume/mcp stdio`);
500 | }
501 | 
502 | // Determine which server mode to run based on command line arguments
503 | const args = process.argv.slice(2);
504 | const shouldRunStdio = args.includes('stdio');
505 | 
506 | if (shouldRunStdio) {
507 |   runStdioServer().catch((error) => {
508 |     console.log("Fatal error running stdio server:", error);
509 |     process.exit(1);
510 |   });
511 | } else {
512 |   runHttpServer().catch((error) => {
513 |     console.log("Fatal error running HTTP server:", error);
514 |     process.exit(1);
515 |   });
516 | }
517 | 
```