JSON Resume MCP Server

Official
  • src
import { Octokit } from "octokit"; import { Resume, sampleResume } from "./types.js"; export class GitHubService { private octokit: Octokit; private username: string; private cachedGistId: string | null = null; constructor(token: string, username: string) { if (!token) { throw new Error("GitHub token is required"); } if (!username) { throw new Error("GitHub username is required"); } this.octokit = new Octokit({ auth: token }); this.username = username; } /** * Fetch user profile information from GitHub */ async getUserProfile() { try { const { data } = await this.octokit.rest.users.getByUsername({ username: this.username, }); return data; } catch (error) { console.error("Error fetching user profile:", error); throw error; } } /** * Get resume.json from user's gists */ async getResumeFromGists(): Promise<Resume | null> { try { // If we've already found the gist ID in this session, use it if (this.cachedGistId) { console.error(`Using cached gist ID: ${this.cachedGistId}`); try { const { data: gist } = await this.octokit.rest.gists.get({ gist_id: this.cachedGistId, }); const files = gist.files || {}; const resumeFile = Object.values(files).find( (file) => file?.filename === "resume.json" ); if (resumeFile && resumeFile.raw_url) { const response = await fetch(resumeFile.raw_url); const resumeData = await response.json(); console.error("Successfully fetched resume from cached gist ID"); return { ...resumeData, _gistId: this.cachedGistId }; } } catch (error) { console.error("Error fetching from cached gist ID, will try listing all gists:", error); this.cachedGistId = null; } } // List all gists for the user console.error(`Listing gists for user: ${this.username}`); const { data: gists } = await this.octokit.rest.gists.list({ username: this.username, per_page: 100, }); console.error(`Found ${gists.length} gists, searching for resume.json`); // Find all gists containing resume.json and sort by updated_at const resumeGists = gists .filter(gist => { const files = gist.files || {}; return Object.values(files).some( (file) => file?.filename === "resume.json" ); }) .sort((a, b) => { // Sort by updated_at in descending order (newest first) return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(); }); if (resumeGists.length > 0) { // Use the most recently updated resume.json gist const mostRecentGist = resumeGists[0]; console.error(`Found ${resumeGists.length} resume.json gists. Using most recent: ${mostRecentGist.id} (updated: ${mostRecentGist.updated_at})`); // Cache the gist ID for future use this.cachedGistId = mostRecentGist.id; const files = mostRecentGist.files || {}; const resumeFile = Object.values(files).find( (file) => file?.filename === "resume.json" ); if (resumeFile && resumeFile.raw_url) { // Fetch the content of resume.json const response = await fetch(resumeFile.raw_url); const resumeData = await response.json(); return { ...resumeData, _gistId: mostRecentGist.id // Store the gist ID for later updates }; } } console.error("No resume.json found in any gists"); return null; // No resume.json found } catch (error) { console.error("Error fetching resume from gists:", error); throw error; } } /** * Create a sample resume.json gist if none exists */ async createSampleResume(): Promise<Resume> { try { // Get user profile to populate some basic fields const userProfile = await this.getUserProfile(); // Create a copy of the sample resume and populate with GitHub profile info const newResume = JSON.parse(JSON.stringify(sampleResume)) as Resume; if (newResume.basics) { newResume.basics.name = userProfile.name || this.username; if (newResume.basics.profiles) { const githubProfile = newResume.basics.profiles.find(p => p.network === "GitHub"); if (githubProfile) { githubProfile.username = this.username; githubProfile.url = `https://github.com/${this.username}`; } } newResume.basics.email = userProfile.email || ""; } console.error("Creating new gist with resume.json"); // Create a new gist with resume.json const { data: gist } = await this.octokit.rest.gists.create({ files: { "resume.json": { content: JSON.stringify(newResume, null, 2), }, }, description: "My JSON Resume", public: true, }); // Cache the gist ID for future use this.cachedGistId = gist.id; console.error(`Created new gist with ID: ${gist.id}`); return { ...newResume, _gistId: gist.id }; } catch (error) { console.error("Error creating sample resume:", error); throw error; } } /** * Update an existing resume.json gist */ async updateResume(resume: Resume): Promise<Resume> { try { // Check if there's a _gistId in the resume let gistId = (resume as any)._gistId; // If not, check our cached gist ID if (!gistId && this.cachedGistId) { console.error(`No _gistId in resume object, using cached gistId: ${this.cachedGistId}`); gistId = this.cachedGistId; } // If we still don't have a gist ID, try to find it if (!gistId) { console.error("No gist ID found, attempting to find existing resume"); const existingResume = await this.getResumeFromGists(); if (existingResume && (existingResume as any)._gistId) { gistId = (existingResume as any)._gistId; console.error(`Found existing resume with gist ID: ${gistId}`); } } // If we still don't have a gist ID, create a new gist if (!gistId) { console.error("No existing resume found, creating a new one"); const newResume = await this.createSampleResume(); gistId = (newResume as any)._gistId; // Merge the existing resume data with our new resume resume = { ...newResume, ...resume, _gistId: gistId }; } // Create a clean copy of the resume without the _gistId property const { _gistId, ...resumeData } = { ...resume, _gistId: undefined }; // Update the lastModified date if (resumeData.meta) { resumeData.meta.lastModified = new Date().toISOString(); } else { resumeData.meta = { lastModified: new Date().toISOString() }; } console.error(`Updating gist with ID: ${gistId}`); // Update the gist const { data: updatedGist } = await this.octokit.rest.gists.update({ gist_id: gistId as string, files: { "resume.json": { content: JSON.stringify(resumeData, null, 2), }, }, }); // Cache the gist ID for future use this.cachedGistId = gistId; return { ...resumeData, _gistId: gistId }; } catch (error) { console.error("Error updating resume gist:", error); throw error; } } /** * Get user's repositories and their contributions */ async getUserRepositories() { try { // Get user's repositories const { data: repos } = await this.octokit.rest.repos.listForUser({ username: this.username, sort: "updated", per_page: 10, // Limit to recent 10 repos }); return repos; } catch (error) { console.error("Error fetching user repositories:", error); throw error; } } /** * Get user's contributions to a specific repository */ async getRepoContributions(owner: string, repo: string) { try { // Get user's commits to the repository const { data: commits } = await this.octokit.rest.repos.listCommits({ owner, repo, author: this.username, per_page: 20, }); return commits; } catch (error) { console.error(`Error fetching contributions to ${owner}/${repo}:`, error); return []; // Return empty array on error } } }