Skip to main content
Glama

NestJS MCP Server Module

by rekog-labs
MIT License
32,416
470
  • Apple
  • Linux
http-streamable-oauth-client.ts12.7 kB
#!/usr/bin/env node import { exec } from 'node:child_process'; import { createServer } from 'node:http'; import { createInterface } from 'node:readline'; import { URL } from 'node:url'; import { OAuthClientProvider, UnauthorizedError, } from '@modelcontextprotocol/sdk/client/auth.js'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { OAuthClientInformation, OAuthClientInformationFull, OAuthClientMetadata, OAuthTokens, } from '@modelcontextprotocol/sdk/shared/auth.js'; import { CallToolRequest, CallToolResultSchema, ListToolsRequest, ListToolsResultSchema, } from '@modelcontextprotocol/sdk/types.js'; // Configuration const DEFAULT_SERVER_URL = 'http://localhost:3030/mcp'; const CALLBACK_PORT = 8090; // Use different port than auth server (3001) const CALLBACK_URL = `http://localhost:${CALLBACK_PORT}/callback`; /** * In-memory OAuth client provider for demonstration purposes * In production, you should persist tokens securely */ class InMemoryOAuthClientProvider implements OAuthClientProvider { private _clientInformation?: OAuthClientInformationFull; private _tokens?: OAuthTokens; private _codeVerifier?: string; constructor( private readonly _redirectUrl: string | URL, private readonly _clientMetadata: OAuthClientMetadata, onRedirect?: (url: URL) => void, ) { this._onRedirect = onRedirect || ((url) => { console.log(`Redirect to: ${url.toString()}`); }); } private _onRedirect: (url: URL) => void; get redirectUrl(): string | URL { return this._redirectUrl; } get clientMetadata(): OAuthClientMetadata { return this._clientMetadata; } clientInformation(): OAuthClientInformation | undefined { return this._clientInformation; } saveClientInformation(clientInformation: OAuthClientInformationFull): void { this._clientInformation = clientInformation; } tokens(): OAuthTokens | undefined { return this._tokens; } saveTokens(tokens: OAuthTokens): void { this._tokens = tokens; } redirectToAuthorization(authorizationUrl: URL): void { this._onRedirect(authorizationUrl); } saveCodeVerifier(codeVerifier: string): void { this._codeVerifier = codeVerifier; } codeVerifier(): string { if (!this._codeVerifier) { throw new Error('No code verifier saved'); } return this._codeVerifier; } } /** * Interactive MCP client with OAuth authentication * Demonstrates the complete OAuth flow with browser-based authorization */ class InteractiveOAuthClient { private client: Client | null = null; private readonly rl = createInterface({ input: process.stdin, output: process.stdout, }); constructor(private serverUrl: string) {} /** * Prompts user for input via readline */ private async question(query: string): Promise<string> { return new Promise((resolve) => { this.rl.question(query, resolve); }); } /** * Opens the authorization URL in the user's default browser */ private openBrowser(url: string): void { console.log(`🌐 Opening browser for authorization: ${url}`); const command = `open "${url}"`; exec(command, (error) => { if (error) { console.error(`Failed to open browser: ${error.message}`); console.log(`Please manually open: ${url}`); } }); } /** * Example OAuth callback handler - in production, use a more robust approach * for handling callbacks and storing tokens */ /** * Starts a temporary HTTP server to receive the OAuth callback */ private async waitForOAuthCallback(): Promise<string> { return new Promise<string>((resolve, reject) => { const server = createServer((req, res) => { // Ignore favicon requests if (req.url === '/favicon.ico') { res.writeHead(404); res.end(); return; } console.log(`📥 Received callback: ${req.url}`); const parsedUrl = new URL(req.url || '', 'http://localhost'); const code = parsedUrl.searchParams.get('code'); const error = parsedUrl.searchParams.get('error'); if (code) { console.log( `✅ Authorization code received: ${code?.substring(0, 10)}...`, ); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(` <html> <body> <h1>Authorization Successful!</h1> <p>You can close this window and return to the terminal.</p> <script>setTimeout(() => window.close(), 2000);</script> </body> </html> `); resolve(code); setTimeout(() => server.close(), 3000); } else if (error) { console.log(`❌ Authorization error: ${error}`); res.writeHead(400, { 'Content-Type': 'text/html' }); res.end(` <html> <body> <h1>Authorization Failed</h1> <p>Error: ${error}</p> </body> </html> `); reject(new Error(`OAuth authorization failed: ${error}`)); } else { console.log(`❌ No authorization code or error in callback`); res.writeHead(400); res.end('Bad request'); reject(new Error('No authorization code provided')); } }); server.listen(CALLBACK_PORT, () => { console.log( `OAuth callback server started on http://localhost:${CALLBACK_PORT}`, ); }); }); } private async attemptConnection( oauthProvider: InMemoryOAuthClientProvider, ): Promise<void> { console.log('🚢 Creating transport with OAuth provider...'); const baseUrl = new URL(this.serverUrl); const transport = new StreamableHTTPClientTransport(baseUrl, { authProvider: oauthProvider, }); console.log('🚢 Transport created'); try { console.log( '🔌 Attempting connection (this will trigger OAuth redirect)...', ); await this.client!.connect(transport); console.log('✅ Connected successfully'); } catch (error) { if (error instanceof UnauthorizedError) { console.log('🔐 OAuth required - waiting for authorization...'); const callbackPromise = this.waitForOAuthCallback(); const authCode = await callbackPromise; await transport.finishAuth(authCode); console.log('🔐 Authorization code received:', authCode); console.log('🔌 Reconnecting with authenticated transport...'); await this.attemptConnection(oauthProvider); } else { console.error('❌ Connection failed with non-auth error:', error); throw error; } } } /** * Establishes connection to the MCP server with OAuth authentication */ async connect(): Promise<void> { console.log(`🔗 Attempting to connect to ${this.serverUrl}...`); const clientMetadata: OAuthClientMetadata = { client_name: 'Simple OAuth MCP Client', redirect_uris: [CALLBACK_URL], grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], token_endpoint_auth_method: 'client_secret_post', scope: 'mcp:tools', }; console.log('🔐 Creating OAuth provider...'); const oauthProvider = new InMemoryOAuthClientProvider( CALLBACK_URL, clientMetadata, (redirectUrl: URL) => { console.log(`📌 OAuth redirect handler called - opening browser`); console.log(`Opening browser to: ${redirectUrl.toString()}`); this.openBrowser(redirectUrl.toString()); }, ); console.log('🔐 OAuth provider created'); console.log('👤 Creating MCP client...'); this.client = new Client( { name: 'simple-oauth-client', version: '1.0.0', }, { capabilities: {} }, ); console.log('👤 Client created'); console.log('🔐 Starting OAuth flow...'); await this.attemptConnection(oauthProvider); // Start interactive loop await this.interactiveLoop(); } /** * Main interactive loop for user commands */ async interactiveLoop(): Promise<void> { console.log('\n🎯 Interactive MCP Client with OAuth'); console.log('Commands:'); console.log(' list - List available tools'); console.log(' call <tool_name> [args] - Call a tool'); console.log(' quit - Exit the client'); console.log(); while (true) { try { const command = await this.question('mcp> '); if (!command.trim()) { continue; } if (command === 'quit') { break; } else if (command === 'list') { await this.listTools(); } else if (command.startsWith('call ')) { await this.handleCallTool(command); } else { console.log( "❌ Unknown command. Try 'list', 'call <tool_name>', or 'quit'", ); } } catch (error) { if (error instanceof Error && error.message === 'SIGINT') { console.log('\n\n👋 Goodbye!'); break; } console.error('❌ Error:', error); } } } private async listTools(): Promise<void> { if (!this.client) { console.log('❌ Not connected to server'); return; } try { const request: ListToolsRequest = { method: 'tools/list', params: {}, }; const result = await this.client.request(request, ListToolsResultSchema); if (result.tools && result.tools.length > 0) { console.log('\n📋 Available tools:'); result.tools.forEach((tool, index) => { console.log(`${index + 1}. ${tool.name}`); if (tool.description) { console.log(` Description: ${tool.description}`); } console.log(); }); } else { console.log('No tools available'); } } catch (error) { console.error('❌ Failed to list tools:', error); } } private async handleCallTool(command: string): Promise<void> { const parts = command.split(/\s+/); const toolName = parts[1]; if (!toolName) { console.log('❌ Please specify a tool name'); return; } // Parse arguments (simple JSON-like format) let toolArgs: Record<string, unknown> = {}; if (parts.length > 2) { const argsString = parts.slice(2).join(' '); try { toolArgs = JSON.parse(argsString); } catch { console.log('❌ Invalid arguments format (expected JSON)'); return; } } await this.callTool(toolName, toolArgs); } private async callTool( toolName: string, toolArgs: Record<string, unknown>, ): Promise<void> { if (!this.client) { console.log('❌ Not connected to server'); return; } try { const request: CallToolRequest = { method: 'tools/call', params: { name: toolName, arguments: toolArgs, }, }; const result = await this.client.request(request, CallToolResultSchema); console.log(`\n🔧 Tool '${toolName}' result:`); if (result.content) { result.content.forEach((content) => { if (content.type === 'text') { console.log(content.text); } else { console.log(content); } }); } else { console.log(result); } } catch (error) { console.error(`❌ Failed to call tool '${toolName}':`, error); } } close(): void { this.rl.close(); if (this.client) { // Note: Client doesn't have a close method in the current implementation // This would typically close the transport connection } } } /** * Main entry point */ async function main(): Promise<void> { const serverUrl = process.env.MCP_SERVER_URL || DEFAULT_SERVER_URL; console.log('🚀 Simple MCP OAuth Client'); console.log(`Connecting to: ${serverUrl}`); console.log(); const client = new InteractiveOAuthClient(serverUrl); // Handle graceful shutdown process.on('SIGINT', () => { console.log('\n\n👋 Goodbye!'); client.close(); process.exit(0); }); try { await client.connect(); } catch (error) { console.error('Failed to start client:', error); process.exit(1); } finally { client.close(); } } // Run if this file is executed directly main().catch((error) => { console.error('Unhandled error:', error); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rekog-labs/MCP-Nest'

If you have feedback or need assistance with the MCP directory API, please join our Discord server