Created
December 24, 2025 09:30
-
-
Save gunzip/adbab8d0968147fcda55024c29a02aff to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import express from 'express'; | |
| import jwt from 'jsonwebtoken'; | |
| import { createClient } from 'redis'; | |
| import { URLSearchParams } from 'url'; | |
| import { createMcpServer, McpServerConfig } from '@modelcontextprotocol/sdk/server'; | |
| import { Request, Response } from 'express'; | |
| const app = express(); | |
| const port = process.env.PORT || 3000; | |
| app.use(express.json()); | |
| // Redis client | |
| const redis = createClient({ url: process.env.REDIS_URL }); | |
| redis.on('error', err => console.error('Redis Error:', err)); | |
| await redis.connect(); | |
| const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-key'; | |
| // Configurazione OAuth providers | |
| const oauthProviders = { | |
| github: { | |
| authorization_endpoint: 'https://github.com/login/oauth/authorize', | |
| token_endpoint: 'https://github.com/login/oauth/access_token', | |
| userinfo_endpoint: 'https://api.github.com/user', | |
| client_id: process.env.GITHUB_CLIENT_ID!, | |
| client_secret: process.env.GITHUB_CLIENT_SECRET!, | |
| scopes: ['user:email'], | |
| redirect_uri: 'https://your-server.com/mcp/callback/github', // cambia con il tuo dominio | |
| }, | |
| notion: { | |
| authorization_endpoint: 'https://www.notion.com/oauth/authorize', | |
| token_endpoint: 'https://api.notion.com/v1/oauth/token', | |
| client_id: process.env.NOTION_CLIENT_ID!, | |
| client_secret: process.env.NOTION_CLIENT_SECRET!, | |
| scopes: ['pages:read', 'pages:write'], | |
| redirect_uri: 'https://your-server.com/mcp/callback/notion', | |
| }, | |
| // Aggiungi qui altri provider se necessario | |
| }; | |
| // Middleware per autenticazione con token interno JWT | |
| function authenticate(req: Request, res: Response, next: Function) { | |
| const authHeader = req.headers.authorization; | |
| if (!authHeader?.startsWith('Bearer ')) { | |
| return res.status(401).json({ error: 'Unauthorized' }); | |
| } | |
| const token = authHeader.split(' ')[1]; | |
| try { | |
| const payload = jwt.verify(token, JWT_SECRET) as { githubLogin: string }; | |
| (req as any).githubLogin = payload.githubLogin; | |
| (req as any).userId = payload.githubLogin; | |
| next(); | |
| } catch (err) { | |
| res.status(401).json({ error: 'Invalid token' }); | |
| } | |
| } | |
| // 1. Inizia login con GitHub | |
| app.get('/mcp/authorize', (req: Request, res: Response) => { | |
| const state = Math.random().toString(36).substring(2); | |
| const authUrl = `${oauthProviders.github.authorization_endpoint}?` + | |
| `client_id=${oauthProviders.github.client_id}&` + | |
| `redirect_uri=${encodeURIComponent(oauthProviders.github.redirect_uri)}&` + | |
| `scope=${oauthProviders.github.scopes.join(' ')}&` + | |
| `state=${state}`; | |
| res.redirect(authUrl); | |
| }); | |
| // 2. Callback GitHub → crea token interno | |
| app.get('/mcp/callback/github', async (req: Request, res: Response) => { | |
| const { code } = req.query; | |
| if (!code) return res.status(400).send('No code received'); | |
| try { | |
| // Scambia code per access_token GitHub | |
| const tokenResponse = await fetch(oauthProviders.github.token_endpoint, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, | |
| body: new URLSearchParams({ | |
| client_id: oauthProviders.github.client_id, | |
| client_secret: oauthProviders.github.client_secret, | |
| code: code as string, | |
| redirect_uri: oauthProviders.github.redirect_uri, | |
| }), | |
| }); | |
| if (!tokenResponse.ok) throw new Error('Failed to exchange code for token'); | |
| const tokenData = await tokenResponse.json(); | |
| const access_token = tokenData.access_token; | |
| // Ottieni info utente GitHub | |
| const userResponse = await fetch(oauthProviders.github.userinfo_endpoint, { | |
| headers: { Authorization: `Bearer ${access_token}` }, | |
| }); | |
| if (!userResponse.ok) throw new Error('Failed to fetch user info'); | |
| const userData = await userResponse.json(); | |
| const githubLogin = userData.login; | |
| // Crea token interno JWT | |
| const internalToken = jwt.sign( | |
| { githubLogin, sub: githubLogin }, | |
| JWT_SECRET, | |
| { expiresIn: '24h' } | |
| ); | |
| // Salva sessione | |
| await redis.set(`session:${githubLogin}`, internalToken, { EX: 86400 }); | |
| // Risposta per l'utente | |
| res.send(` | |
| <html> | |
| <body> | |
| <h1>Benvenuto, @${githubLogin}!</h1> | |
| <p>Autorizzazione completata.</p> | |
| <p>Chiudi questa finestra e torna all'app.</p> | |
| <script> | |
| if (window.location.href.includes('claude://')) { | |
| window.location = "claude://authorized?token=${internalToken}"; | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| `); | |
| } catch (err) { | |
| console.error(err); | |
| res.status(500).send('Errore durante l\'autorizzazione'); | |
| } | |
| }); | |
| // 3. Autorizza un provider specifico (es. Notion) | |
| app.get('/mcp/authorize-provider', authenticate, (req: Request, res: Response) => { | |
| const { provider } = req.query as { provider: string }; | |
| if (!provider || !oauthProviders[provider]) { | |
| return res.status(400).json({ error: 'Provider non supportato' }); | |
| } | |
| const state = Math.random().toString(36).substring(2); | |
| const authUrl = `${oauthProviders[provider].authorization_endpoint}?` + | |
| `client_id=${oauthProviders[provider].client_id}&` + | |
| `redirect_uri=${encodeURIComponent(oauthProviders[provider].redirect_uri)}&` + | |
| `response_type=code&` + | |
| `scope=${oauthProviders[provider].scopes.join(' ')}&` + | |
| `state=${state}`; | |
| res.redirect(authUrl); | |
| }); | |
| // 4. Callback per provider esterno | |
| app.get('/mcp/callback/:provider', authenticate, async (req: Request, res: Response) => { | |
| const { provider } = req.params; | |
| const { code } = req.query; | |
| if (!code || !oauthProviders[provider]) return res.status(400).send('Callback non valido'); | |
| try { | |
| const userId = (req as any).githubLogin; | |
| const tokenResponse = await fetch(oauthProviders[provider].token_endpoint, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, | |
| body: new URLSearchParams({ | |
| code: code as string, | |
| client_id: oauthProviders[provider].client_id, | |
| client_secret: oauthProviders[provider].client_secret, | |
| redirect_uri: oauthProviders[provider].redirect_uri, | |
| grant_type: 'authorization_code', | |
| }), | |
| }); | |
| if (!tokenResponse.ok) throw new Error('Failed to exchange code'); | |
| const tokenData = await tokenResponse.json(); | |
| const access_token = tokenData.access_token; | |
| const refresh_token = tokenData.refresh_token; | |
| const expires_in = tokenData.expires_in || 3600; | |
| // Memorizza token | |
| await redis.set(`token:${userId}:${provider}`, access_token, { EX: expires_in }); | |
| if (refresh_token) { | |
| await redis.set(`refresh:${userId}:${provider}`, refresh_token); | |
| } | |
| res.send(` | |
| <html> | |
| <body> | |
| <h1>${provider} autorizzato!</h1> | |
| <p>Torna all'applicazione.</p> | |
| </body> | |
| </html> | |
| `); | |
| } catch (err) { | |
| console.error(err); | |
| res.status(500).send('Errore'); | |
| } | |
| }); | |
| // 5. Esempio tool: Crea pagina Notion | |
| const notionTool = { | |
| name: 'create_notion_page', | |
| description: 'Crea una nuova pagina in Notion', | |
| parameters: { | |
| title: { type: 'string', required: true }, | |
| content: { type: 'string', required: true }, | |
| }, | |
| execute: async (params: { title: string; content: string }, context: any) => { | |
| const userId = context.user?.githubLogin || (context.request as any).githubLogin; | |
| if (!userId) throw new Error('Utente non autenticato'); | |
| let accessToken = await redis.get(`token:${userId}:notion`); | |
| if (!accessToken) { | |
| throw { | |
| status: 401, | |
| headers: { 'WWW-Authenticate': 'Bearer realm="notion"' }, | |
| body: { error: 'login_required', provider: 'notion' }, | |
| }; | |
| } | |
| // Chiama Notion API | |
| const response = await fetch('https://api.notion.com/v1/pages', { | |
| method: 'POST', | |
| headers: { | |
| Authorization: `Bearer ${accessToken}`, | |
| 'Notion-Version': '2022-06-28', | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| parent: { page_id: 'YOUR_PARENT_PAGE_ID' }, // cambia con il tuo | |
| properties: { title: [{ text: { content: params.title } }] }, | |
| children: [{ paragraph: { rich_text: [{ text: { content: params.content } }] } }], | |
| }), | |
| }); | |
| if (!response.ok) { | |
| if (response.status === 401) { | |
| // Potresti implementare qui il refresh token | |
| throw { status: 401, body: { error: 'token_expired' } }; | |
| } | |
| throw new Error('Errore Notion API'); | |
| } | |
| const data = await response.json(); | |
| return { page_id: data.id, url: data.url }; | |
| }, | |
| }; | |
| // Configurazione MCP Server | |
| const mcpConfig: McpServerConfig = { | |
| tools: [notionTool], | |
| protectedResourceMetadata: true, | |
| }; | |
| const mcpServer = createMcpServer(mcpConfig); | |
| // Proteggi tutte le route MCP con autenticazione | |
| app.use('/mcp', authenticate, mcpServer.handler); | |
| app.listen(port, () => { | |
| console.log(`Server MCP avviato su http://localhost:${port}/mcp`); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment