@@ -5,6 +5,7 @@ const os = require('os');
55const express = require ( 'express' ) ;
66const { spawn } = require ( 'child_process' ) ;
77const { PostHog } = require ( 'posthog-node' ) ;
8+ const crypto = require ( 'crypto' ) ;
89
910const POSTHOG_KEY = process . env . MCPKIT_POSTHOG_KEY || '' ;
1011const POSTHOG_HOST = process . env . MCPKIT_POSTHOG_HOST || 'https://us.i.posthog.com' ;
@@ -38,6 +39,13 @@ const PERSISTENCE_FILE = path.join(os.homedir(), '.mcpkit', 'agents.json');
3839// MCP registry file
3940const MCP_REGISTRY_FILE = path . join ( __dirname , '..' , 'mcp-registry.json' ) ;
4041
42+ // OAuth config (GitHub)
43+ const GITHUB_CLIENT_ID = process . env . MCPKIT_GITHUB_CLIENT_ID || 'Ov23liPIjxP8XsjeUoik' ;
44+ const GITHUB_CLIENT_SECRET = process . env . MCPKIT_GITHUB_CLIENT_SECRET || '' ;
45+ const OAUTH_CALLBACK_PATH = '/auth/github/callback' ;
46+ const SESSION_SECRET = process . env . MCPKIT_SESSION_SECRET || 'dev_session_secret_change_me' ;
47+ const REGISTRY_BASE = 'https://registry.modelcontextprotocol.io' ;
48+
4149function ensurePersistenceDir ( ) {
4250 const dir = path . dirname ( PERSISTENCE_FILE ) ;
4351 if ( ! fs . existsSync ( dir ) ) {
@@ -631,7 +639,8 @@ function transformOfficialServerToInternal(server) {
631639 const name = server . name || server . display_name || server . title || id ;
632640 const description = server . description || '' ;
633641 const version = server . version || 'latest' ;
634- const documentation = server . documentation || server . homepage || ( server . repository && server . repository . url ) || '' ;
642+ const documentation = server . documentation || '' ;
643+ const homepage = server . homepage || '' ;
635644 const npmPkg = ( server . packages || [ ] ) . find ( p => p . registry_type === 'npm' ) ;
636645 const identifier = npmPkg ?. identifier || null ;
637646 const command = identifier ? `npx ${ identifier } @${ version || 'latest' } ` : null ;
@@ -648,9 +657,15 @@ function transformOfficialServerToInternal(server) {
648657 setup_instructions : [ ] ,
649658 uninstall_steps : [ ] ,
650659 documentation,
660+ homepage,
651661 repository : server . repository && server . repository . url ? server . repository . url : null ,
652662 remotes : Array . isArray ( server . remotes ) ? server . remotes : [ ] ,
653663 packages : Array . isArray ( server . packages ) ? server . packages : [ ] ,
664+ registry_type : npmPkg ?. registry_type || null ,
665+ registry_base_url : npmPkg ?. registry_base_url || null ,
666+ license : server . license || null ,
667+ published_at : officialMeta && officialMeta . published_at ? officialMeta . published_at : null ,
668+ updated_at : officialMeta && officialMeta . updated_at ? officialMeta . updated_at : null ,
654669 installed : false ,
655670 installation_date : null ,
656671 installed_agents : [ ]
@@ -759,6 +774,34 @@ function removeServerFromMcpConfig(mcpConfigPath, serverId, agentId = 'cursor')
759774async function main ( ) {
760775 const app = express ( ) ;
761776 app . use ( express . json ( ) ) ;
777+ // simple cookie parser
778+ app . use ( ( req , res , next ) => {
779+ const cookie = req . headers . cookie || '' ;
780+ const map = { } ;
781+ cookie . split ( ';' ) . forEach ( p => { const i = p . indexOf ( '=' ) ; if ( i > 0 ) { const k = p . slice ( 0 , i ) . trim ( ) ; const v = decodeURIComponent ( p . slice ( i + 1 ) ) ; map [ k ] = v ; } } ) ;
782+ req . cookies = map ;
783+ next ( ) ;
784+ } ) ;
785+
786+ // in-memory session store
787+ const sessions = new Map ( ) ;
788+ function sign ( value ) {
789+ return crypto . createHmac ( 'sha256' , SESSION_SECRET ) . update ( value ) . digest ( 'hex' ) ;
790+ }
791+ function setSession ( res , data ) {
792+ const id = crypto . randomBytes ( 16 ) . toString ( 'hex' ) ;
793+ sessions . set ( id , { ...data , createdAt : Date . now ( ) } ) ;
794+ const sig = sign ( id ) ;
795+ res . setHeader ( 'Set-Cookie' , `mcpkit_sid=${ id } .${ sig } ; Path=/; HttpOnly; SameSite=Lax` ) ;
796+ }
797+ function getSession ( req ) {
798+ const raw = req . cookies [ 'mcpkit_sid' ] || '' ;
799+ const [ id , sig ] = raw . split ( '.' ) ;
800+ if ( ! id || ! sig ) return null ;
801+ if ( sign ( id ) !== sig ) return null ;
802+ const data = sessions . get ( id ) ;
803+ return data || null ;
804+ }
762805
763806 // Public telemetry config for frontend initialization
764807 app . get ( '/api/telemetry-config' , ( req , res ) => {
@@ -776,6 +819,121 @@ async function main() {
776819 res . json ( { agents } ) ;
777820 } ) ;
778821
822+ // OAuth: start GitHub login
823+ app . get ( '/auth/github/login' , ( req , res ) => {
824+ if ( ! GITHUB_CLIENT_ID ) return res . status ( 500 ) . send ( 'GitHub OAuth not configured' ) ;
825+ const redirectUri = `${ req . protocol } ://${ req . get ( 'host' ) } ${ OAUTH_CALLBACK_PATH } ` ;
826+ const state = crypto . randomBytes ( 8 ) . toString ( 'hex' ) ;
827+ const url = `https://github.com/login/oauth/authorize?client_id=${ encodeURIComponent ( GITHUB_CLIENT_ID ) } &redirect_uri=${ encodeURIComponent ( redirectUri ) } &scope=read:user%20user:email&state=${ state } ` ;
828+ res . redirect ( url ) ;
829+ } ) ;
830+
831+ // GitHub Device Flow - start
832+ app . post ( '/auth/github/device/start' , async ( req , res ) => {
833+ try {
834+ if ( ! GITHUB_CLIENT_ID ) return res . status ( 500 ) . json ( { error : 'github_not_configured' } ) ;
835+ const body = new URLSearchParams ( ) ;
836+ body . set ( 'client_id' , GITHUB_CLIENT_ID ) ;
837+ body . set ( 'scope' , 'read:user user:email' ) ;
838+ const r = await fetch ( 'https://github.com/login/device/code' , {
839+ method : 'POST' ,
840+ headers : { 'Content-Type' : 'application/x-www-form-urlencoded' , 'Accept' : 'application/json' } ,
841+ body
842+ } ) ;
843+ const data = await r . json ( ) ;
844+ if ( ! r . ok ) return res . status ( r . status ) . json ( data ) ;
845+ // returns device_code, user_code, verification_uri, interval, expires_in
846+ res . json ( data ) ;
847+ } catch ( e ) {
848+ res . status ( 500 ) . json ( { error : e . message } ) ;
849+ }
850+ } ) ;
851+
852+ // GitHub Device Flow - poll
853+ app . post ( '/auth/github/device/poll' , async ( req , res ) => {
854+ try {
855+ if ( ! GITHUB_CLIENT_ID ) return res . status ( 500 ) . json ( { error : 'github_not_configured' } ) ;
856+ const { device_code } = req . body || { } ;
857+ if ( ! device_code ) return res . status ( 400 ) . json ( { error : 'device_code_required' } ) ;
858+ const body = new URLSearchParams ( ) ;
859+ body . set ( 'client_id' , GITHUB_CLIENT_ID ) ;
860+ body . set ( 'device_code' , device_code ) ;
861+ body . set ( 'grant_type' , 'urn:ietf:params:oauth:grant-type:device_code' ) ;
862+ const r = await fetch ( 'https://github.com/login/oauth/access_token' , {
863+ method : 'POST' ,
864+ headers : { 'Content-Type' : 'application/x-www-form-urlencoded' , 'Accept' : 'application/json' } ,
865+ body
866+ } ) ;
867+ const data = await r . json ( ) ;
868+ // Possible responses: {access_token,...} or {error: 'authorization_pending'|'slow_down'|'expired_token'|'access_denied'}
869+ if ( data . access_token ) {
870+ const ghToken = data . access_token ;
871+ const userRes = await fetch ( 'https://api.github.com/user' , { headers : { Authorization : `Bearer ${ ghToken } ` , 'User-Agent' : 'mcpkit' } } ) ;
872+ const user = await userRes . json ( ) ;
873+ // create session
874+ setSession ( res , { provider : 'github' , token : ghToken , user } ) ;
875+ return res . json ( { authenticated : true , user } ) ;
876+ }
877+ if ( data . error === 'authorization_pending' || data . error === 'slow_down' ) {
878+ return res . json ( { authenticated : false , pending : true } ) ;
879+ }
880+ return res . status ( 400 ) . json ( { authenticated : false , error : data . error || 'unknown_error' } ) ;
881+ } catch ( e ) {
882+ res . status ( 500 ) . json ( { error : e . message } ) ;
883+ }
884+ } ) ;
885+
886+ // OAuth: GitHub callback
887+ app . get ( OAUTH_CALLBACK_PATH , async ( req , res ) => {
888+ try {
889+ const code = req . query . code ;
890+ if ( ! code ) return res . status ( 400 ) . send ( 'Missing code' ) ;
891+ const redirectUri = `${ req . protocol } ://${ req . get ( 'host' ) } ${ OAUTH_CALLBACK_PATH } ` ;
892+ const tokenRes = await fetch ( 'https://github.com/login/oauth/access_token' , {
893+ method : 'POST' ,
894+ headers : { 'Content-Type' : 'application/json' , 'Accept' : 'application/json' } ,
895+ body : JSON . stringify ( { client_id : GITHUB_CLIENT_ID , client_secret : GITHUB_CLIENT_SECRET , code, redirect_uri : redirectUri } )
896+ } ) ;
897+ const tokenJson = await tokenRes . json ( ) ;
898+ if ( ! tokenRes . ok || ! tokenJson . access_token ) return res . status ( 400 ) . send ( 'OAuth exchange failed' ) ;
899+ const ghToken = tokenJson . access_token ;
900+ // fetch user
901+ const userRes = await fetch ( 'https://api.github.com/user' , { headers : { Authorization : `Bearer ${ ghToken } ` , 'User-Agent' : 'mcpkit' } } ) ;
902+ const user = await userRes . json ( ) ;
903+ setSession ( res , { provider : 'github' , token : ghToken , user } ) ;
904+ res . send ( '<script>window.opener && window.opener.postMessage({type:"oauth_complete"}, "*"); window.close();</script>' ) ;
905+ } catch ( e ) {
906+ res . status ( 500 ) . send ( 'OAuth error' ) ;
907+ }
908+ } ) ;
909+
910+ // Current user info
911+ app . get ( '/api/me' , ( req , res ) => {
912+ const s = getSession ( req ) ;
913+ if ( ! s ) return res . json ( { authenticated : false } ) ;
914+ res . json ( { authenticated : true , provider : s . provider , user : s . user } ) ;
915+ } ) ;
916+
917+ // Publish server proxy to official registry
918+ app . post ( '/api/publish-server' , async ( req , res ) => {
919+ try {
920+ const s = getSession ( req ) ;
921+ if ( ! s ) return res . status ( 401 ) . json ( { error : 'unauthorized' } ) ;
922+ const serverJson = req . body ;
923+ // Forward to official registry publish endpoint
924+ const r = await fetch ( `${ REGISTRY_BASE } /v0/servers` , {
925+ method : 'POST' ,
926+ headers : { 'Content-Type' : 'application/json' } ,
927+ body : JSON . stringify ( serverJson )
928+ } ) ;
929+ const data = await r . json ( ) . catch ( ( ) => ( { } ) ) ;
930+ if ( ! r . ok ) return res . status ( r . status ) . json ( { error : 'registry_error' , data } ) ;
931+ res . json ( { ok : true , server : data } ) ;
932+ } catch ( e ) {
933+ res . status ( 500 ) . json ( { error : e . message } ) ;
934+ }
935+ } ) ;
936+
779937 // Proxy list from official registry with cursor
780938 app . get ( '/api/official/servers' , async ( req , res ) => {
781939 try {
0 commit comments