UNPKG

gaunt-sloth-assistant

Version:

[![Tests and Lint](https://github.com/Galvanized-Pukeko/gaunt-sloth-assistant/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/Galvanized-Pukeko/gaunt-sloth-assistant/actions/workflows/unit-tests.yml) [![Integration Tests](https://github.co

197 lines 8.2 kB
import { auth } from '@modelcontextprotocol/sdk/client/auth.js'; import express from 'express'; import * as crypto from 'node:crypto'; import { platform } from 'node:os'; import { execSync } from 'node:child_process'; import { displayInfo } from '#src/consoleUtils.js'; import { readFileSync, writeFileSync, existsSync } from 'node:fs'; import { getOAuthStoragePath } from '#src/globalConfigUtils.js'; /** * Please note most of these "unused" methods are part of {@link OAuthClientProvider} */ export class OAuthClientProviderImpl { config; innerState; storagePath; constructor(config) { this.config = config; this.innerState = crypto.randomUUID(); if (!this.config.redirectUrl) { throw new Error('No redirect URL provided'); } if (!this.config.serverUrl) { throw new Error('No server URL provided'); } this.storagePath = getOAuthStoragePath(this.config.serverUrl); } loadStorageData() { if (existsSync(this.storagePath)) { try { const data = readFileSync(this.storagePath, 'utf-8'); return JSON.parse(data); } catch (error) { displayInfo('Failed to load OAuth storage data:' + error); return {}; } } return {}; } saveStorageData(data) { try { writeFileSync(this.storagePath, JSON.stringify(data, null, 2), 'utf-8'); } catch (error) { console.error('Failed to save OAuth storage data:', error); } } state() { return this.innerState; } // noinspection JSUnusedGlobalSymbols get redirectUrl() { return this.config.redirectUrl; } // noinspection JSUnusedGlobalSymbols get clientMetadata() { return { redirect_uris: [this.config.redirectUrl], client_name: 'Gaunt Sloth Assistant', client_uri: 'https://github.com/Galvanized-Pukeko/gaunt-sloth-assistant', software_id: '1dd38b83-946b-4631-8855-66ee467bfd68', scope: 'mcp:read mcp:write', token_endpoint_auth_method: 'none', grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], }; } // noinspection JSUnusedGlobalSymbols saveClientInformation(clientInformation) { const data = this.loadStorageData(); data.clientInformation = clientInformation; this.saveStorageData(data); return Promise.resolve(); } // noinspection JSUnusedGlobalSymbols async clientInformation() { const data = this.loadStorageData(); return Promise.resolve(data.clientInformation); } // noinspection JSUnusedGlobalSymbols async saveTokens(tokens) { const data = this.loadStorageData(); data.tokens = tokens; this.saveStorageData(data); } // noinspection JSUnusedGlobalSymbols tokens() { const data = this.loadStorageData(); return data.tokens; } // noinspection JSUnusedGlobalSymbols saveCodeVerifier(codeVerifier) { const data = this.loadStorageData(); data.codeVerifier = codeVerifier; this.saveStorageData(data); return Promise.resolve(); } // noinspection JSUnusedGlobalSymbols codeVerifier() { const data = this.loadStorageData(); if (!data.codeVerifier) { throw new Error('No code verifier stored'); } return Promise.resolve(data.codeVerifier); } // noinspection JSUnusedGlobalSymbols async redirectToAuthorization(authUrl) { displayInfo('Auth url: ' + authUrl.toString()); try { const url = authUrl.toString(); displayInfo('Trying to open browser'); // Handle different platforms const platformName = platform(); if (platformName === 'win32') { // Windows execSync(`start "" "${url}"`); } else if (platformName === 'darwin') { // macOS execSync(`open "${url}"`); } else { // Linux and others execSync(`xdg-open "${url}"`); } } catch (error) { displayInfo(`Failed to open browser: ${error}`); displayInfo(`Please open ${authUrl.toString()} in your browser`); } } } export async function createAuthProviderAndAuthenticate(mcpServer) { const { port, server, codePromise } = await createOAuthRedirectServer('/oauth-callback'); const authProvider = new OAuthClientProviderImpl({ redirectUrl: `http://127.0.0.1:${port}/oauth-callback`, serverUrl: mcpServer.url, }); const outcome = await auth(authProvider, { serverUrl: mcpServer.url }); if (outcome == 'REDIRECT') { const authorizationCode = await codePromise; await auth(authProvider, { serverUrl: mcpServer.url, authorizationCode }); } else if (outcome == 'AUTHORIZED') { try { server.close(); } catch { } displayInfo('Authorized'); } else { throw new Error(`Unexpected Auth outcome: ${outcome}`); } return authProvider; } export function createOAuthRedirectServer(path, portParam = 0) { const redirectApp = express(); return new Promise((resolve, reject) => { const codePromise = new Promise((resolveCode, rejectCode) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any redirectApp.get(path, (req, res) => { const code = req.query.code; if (!code) { res.status(400).send('Error: No auth code received'); rejectCode('Error: No auth code received'); return; } res.send(`<div style="height: 80vh;text-align: center;display: flex;justify-content: center;align-items: center;"> <div> <h1>Auth successful!</h1> You may close this window and return to the Gaunt Sloth. <div> <img src="data:image/bmp;base64,Qk02AwAAAAAAADYAAAAoAAAAEAAAABAAAAABABgAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAnD98ZBtDTQ8sey9YOQobRA8nZx9OdyxahzZrMwERdCtShjZqLQUVLwgZKwUWKQYXsFCRq06KciJPnEh/n0GEmjuClDl9lzl/uFGglz56dChOm0h0SBInLgQVKwgZKgUVgzZchDRfqEeLsUmavFWjvV6joEGFizFvsU+Yw2CowWCiu1qdbCRNKgEQMAofMAcebSRIgC9bt1KdqEqNnD6BokGJqEaOvVmjqEWMu12ftFeYwGCjpUOMUxc8KQIRLAQWhjVmo0qGs1CYqEmOqEmPmjyArk2UrEyQr02TsU2WwV6pvF2eo0CKlDl+OgsiJwQSijZthS9qnDuAo0GFqESMmz2BijZtpUOLnT2CuVicyWmtsVCUjDh0p0KQbiVYJAQSkzx3kzZ7iS5rv2mn3Y3Gq0+Pgi1laCFOfyxmmTx/slCVmT55eihemzyGjTd6NwgfgSxokzl9qkuY4pLR+Lvg1H3EjTRzZB1IjTV3cCNWmjmAgC1diCxpjC1ulDd8RhAtdihbljl/vmK1v2WuuWGgtVWdjDVwgS5ldCVZcydckjl5kzh3ynGwxHOooT6DSA8sQQwkpUeTyGrBlTp8eCNZkzh1rEeSmDx9ZB9JcyNYkzh2tlek5pjT8q3bqU2PPQokJgEPhTVxy2nBkDd6kDV2qEaOwGOvvWGxuViqoUSNlDd6nj6FvmKq13zKnkiJMgUZKwUWPg4poUaOkzp+kTh7nkCExG60033Jym7C03zKsVOdiC9tjjR10W7IeThrKAAOMAUYJgUULgUZdCZdqkqYu1ersk+fvFyww2q4z3fDqUmXhy5ulTp/yWi/Txo6JgEPLgYZLQUXKQUVJwkaSxExq1GTxWW3t1SpsU+gv1mvnUOLjTJ3izh3dTRkJwEQKgQWLQYaLAgcLQUYLQYZLAERezZaiTtmXyFJgjp0ijt1iTlqOAsjNQ0pKQASJwUVKQYYLgUZLAQZLAgcLgUZMAUYZyVRfCthPQoiIQAJOgcgYyBLKAIRLAUZNwsoMQcfKAQV" width="16" height="16"> </div> </div> </div>`); resolveCode(code); if (server) { displayInfo('Cleaning auth redirect server...'); server.close(); } }); }); const server = redirectApp.listen(portParam, () => { const addressInfo = server.address(); const port = addressInfo.port; displayInfo(`OAuth callback server listening at ${port}`); resolve({ port, server, codePromise }); }); server.on('error', (err) => { // If the server fails to start, reject the outer promise. reject(err); }); }); } //# sourceMappingURL=OAuthClientProviderImpl.js.map