import { Tool, createAuthError } from '@leanmcp/core';
import { TokenVerifier } from '@leanmcp/auth/server';
export class GitHubService {
private verifier: TokenVerifier;
constructor() {
const publicUrl = process.env.PUBLIC_URL || 'http://localhost:3300';
const jwtSigningSecret = process.env.JWT_SIGNING_SECRET || process.env.SESSION_SECRET;
const jwtEncryptionSecret = Buffer.from(
process.env.JWT_ENCRYPTION_SECRET ||
process.env.SESSION_SECRET.padEnd(64, '0').slice(0, 64),
'hex'
);
this.verifier = new TokenVerifier({
issuer: publicUrl,
audience: publicUrl,
secret: jwtSigningSecret,
encryptionSecret: jwtEncryptionSecret,
});
}
/**
* Decrypt upstream GitHub token from JWT
* ChatGPT sends JWT in _meta.authToken
*/
private async getAccessToken(meta?: { authToken?: string }): Promise<string | null> {
if (meta?.authToken) {
try {
const result = await this.verifier.verify(meta.authToken);
if (result.valid && result.upstreamToken) {
// Successfully decrypted GitHub token
return result.upstreamToken;
}
} catch (error: any) {
console.warn('[GitHubService] Token verification error:', error.message);
}
}
// Fallback to env for local testing
return process.env.GITHUB_TOKEN || null;
}
@Tool({
description: 'Fetch GitHub profile for authenticated user',
securitySchemes: [
{ type: 'noauth' },
{ type: 'oauth2', scopes: ['read:user'] }
],
})
async fetchGitHubProfile(input: ProfileInput, meta?: { authToken?: string }) {
const token = await this.getAccessToken(meta);
if (!token) {
// Return MCP auth error to trigger ChatGPT OAuth UI
return createAuthError(
'Please authenticate with GitHub to view profile data',
{
resourceMetadataUrl: `${process.env.PUBLIC_URL}/.well-known/oauth-protected-resource`,
error: 'invalid_token',
errorDescription: 'No access token provided',
}
);
}
try {
const url = input.username
? `https://api.github.com/users/${input.username}`
: 'https://api.github.com/user';
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/vnd.github.v3+json',
},
});
const data = await response.json();
return {
success: true,
profile: {
login: data.login,
name: data.name,
avatarUrl: data.avatar_url,
bio: data.bio,
publicRepos: data.public_repos,
followers: data.followers,
following: data.following,
},
};
} catch (error: any) {
if (error.message.includes('401')) {
return createAuthError(
'Your GitHub session has expired. Please re-authenticate.',
{
resourceMetadataUrl: `${process.env.PUBLIC_URL}/.well-known/oauth-protected-resource`,
error: 'expired_token',
}
);
}
return { success: false, error: error.message };
}
}
@Tool({
description: 'Fetch all repositories for authenticated user',
securitySchemes: [
{ type: 'oauth2', scopes: ['read:user', 'repo'] }
],
})
async fetchGitHubRepos(input: ProfileInput, meta?: { authToken?: string }) {
const token = await this.getAccessToken(meta);
if (!token) {
return createAuthError('Authentication required', {
resourceMetadataUrl: `${process.env.PUBLIC_URL}/.well-known/oauth-protected-resource`,
});
}
const url = input.username
? `https://api.github.com/users/${input.username}/repos?per_page=100`
: 'https://api.github.com/user/repos?per_page=100';
const response = await fetch(url, {
headers: { 'Authorization': `Bearer ${token}` },
});
const data = await response.json();
return {
success: true,
repos: data.map((repo: any) => ({
name: repo.name,
stars: repo.stargazers_count,
forks: repo.forks_count,
language: repo.language,
isFork: repo.fork,
})),
};
}
}