Skip to content

Commit 6b400e6

Browse files
committed
added oauth
1 parent a8c34af commit 6b400e6

File tree

3 files changed

+3153
-330
lines changed

3 files changed

+3153
-330
lines changed

bin/mcp-kit.js

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const os = require('os');
55
const express = require('express');
66
const { spawn } = require('child_process');
77
const { PostHog } = require('posthog-node');
8+
const crypto = require('crypto');
89

910
const POSTHOG_KEY = process.env.MCPKIT_POSTHOG_KEY || '';
1011
const 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
3940
const 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+
4149
function 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')
759774
async 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

Comments
 (0)