Skip to main content
Glama
Terminal-Bridge.tsx•21.5 kB
import React, { useEffect, useRef, useState, useCallback } from 'react'; import { Terminal as Xterm } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; import 'xterm/css/xterm.css'; import { useTool } from '@modelcontextprotocol/sdk/react'; import Console from './Console.js'; import { TenantToolBridge } from '../src/tenant-tool-bridge.js'; import MultiTenantOrchestrator from '../src/multi-tenant-orchestrator.js'; type Tab = 'terminal' | 'console' | 'session'; interface Session { id: string; status: 'active' | 'idle' | 'expired'; workspace: string; gitConfig: { name?: string; email?: string; pat?: string; }; createdAt: string; lastActivity: string; } export const TerminalBridge: React.FC<{ terminalOutput: string[]; setTerminalOutput: React.Dispatch<React.SetStateAction<string[]>>; currentSession: Session | null; orchestrator: MultiTenantOrchestrator; }> = ({ terminalOutput, setTerminalOutput, currentSession, orchestrator }) => { const termElRef = useRef<HTMLDivElement>(null); const termRef = useRef<Xterm | null>(null); const fitAddonRef = useRef<FitAddon | null>(null); const [activeTab, setActiveTab] = useState<Tab>('terminal'); const [cwd, setCwd] = useState('/workspace'); const commandHistory = useRef<string[]>([]); const historyIndex = useRef<number>(-1); const [gitConfig, setGitConfig] = useState({ name: '', email: '', pat: '' }); const [bridgeStatus, setBridgeStatus] = useState<any>(null); // Get tool bridge from orchestrator const toolBridge = orchestrator.getToolBridge(); const termWrite = (text: string) => termRef.current?.write(text.replace(/\n/g, '\r\n')); const getPrompt = (path: string) => `\r\n\x1b[1;34mpyforge-bridge\x1b[0m:\x1b[1;32m${path}\x1b[0m$ `; // Create new session const handleCreateSession = useCallback(async () => { try { const session = await orchestrator.createSession({ name: gitConfig.name || undefined, email: gitConfig.email || undefined, pat: gitConfig.pat || undefined }); termWrite(`āœ… Session created: ${session.id.substring(0, 8)}...\r\n`); termWrite(`šŸ“ Workspace: ${session.workspace}\r\n`); setCwd('/workspace'); // Update bridge status const status = await toolBridge.getBridgeStatus(); setBridgeStatus(status); termWrite(`šŸŒ‰ Bridge Status: ${status.status}\r\n`); } catch (error: any) { termWrite(`āŒ Error creating session: ${error.message}\r\n`); } }, [gitConfig, orchestrator, toolBridge]); // Update session git config const handleUpdateGitConfig = useCallback(async () => { if (!currentSession) { termWrite(`āŒ No active session. Create a session first.\r\n`); return; } try { const success = await orchestrator.updateSessionGitConfig(currentSession.id, { name: gitConfig.name || undefined, email: gitConfig.email || undefined, pat: gitConfig.pat || undefined }); if (success) { termWrite(`āœ… Git configuration updated for session ${currentSession.id.substring(0, 8)}...\r\n`); } else { termWrite(`āŒ Failed to update git config\r\n`); } } catch (error: any) { termWrite(`āŒ Error updating git config: ${error.message}\r\n`); } }, [currentSession, gitConfig, orchestrator]); // Show session info const handleShowSessions = useCallback(() => { try { const info = orchestrator.getSessionInfo(); termWrite(`šŸ“Š Session Overview:\r\n`); termWrite(` Total Sessions: ${info.totalSessions}\r\n`); termWrite(` Active Sessions: ${info.activeSessions}\r\n\r\n`); info.sessions.forEach((session: any) => { termWrite(` šŸ·ļø ${session.id} - ${session.status} (${session.age})\r\n`); }); } catch (error: any) { termWrite(`āŒ Error getting session info: ${error.message}\r\n`); } }, [orchestrator]); // Test bridge connectivity const handleTestBridge = useCallback(async () => { try { termWrite(`šŸŒ‰ Testing bridge connectivity...\r\n`); const status = await toolBridge.getBridgeStatus(); setBridgeStatus(status); termWrite(`šŸ”— Bridge Status: ${status.status}\r\n`); termWrite(`šŸ“” Server Universal: ${status.serverUniversalConnected ? 'āœ… Connected' : 'āŒ Disconnected'}\r\n`); if (status.error) { termWrite(`āŒ Bridge Error: ${status.error}\r\n`); } const tools = status.availableTools || []; termWrite(`šŸ› ļø Available Tools: ${tools.join(', ')}\r\n`); } catch (error: any) { termWrite(`āŒ Bridge test failed: ${error.message}\r\n`); } }, [toolBridge]); // Execute command through bridge const handleBridgeCommand = useCallback(async (command: string, sessionId: string) => { try { termWrite(`šŸŒ‰ Executing via bridge: ${command}\r\n`); const result = await toolBridge.executeTool(sessionId, 'bash', { command }); if (result.success) { if (result.stdout) { termWrite(`${result.stdout}\r\n`); } if (result.stderr) { termWrite(`āš ļø ${result.stderr}\r\n`); } } else { termWrite(`āŒ Command failed: ${result.message}\r\n`); if (result.stderr) { termWrite(`Error output: ${result.stderr}\r\n`); } } } catch (error: any) { termWrite(`āŒ Bridge execution error: ${error.message}\r\n`); } }, [toolBridge]); // Execute database command through bridge const handleDatabaseCommand = useCallback(async (command: string, sessionId: string) => { try { termWrite(`šŸ” Searching database: ${command}\r\n`); const result = await toolBridge.executeTool(sessionId, 'search_local', { query: command }); if (result.success && result.data) { termWrite(`šŸ“Š Search Results:\r\n`); termWrite(`${JSON.stringify(result.data, null, 2)}\r\n`); } else { termWrite(`āŒ Search failed: ${result.message}\r\n`); } } catch (error: any) { termWrite(`āŒ Database search error: ${error.message}\r\n`); } }, [toolBridge]); useEffect(() => { if (!termElRef.current || termRef.current) return; const term = new Xterm({ cursorBlink: true, fontFamily: 'monospace', fontSize: 13, theme: { background: 'var(--background-primary)', foreground: 'var(--text-primary)', cursor: 'var(--accent-primary)' }, }); const fitAddon = new FitAddon(); term.loadAddon(fitAddon); term.open(termElRef.current); fitAddon.fit(); termRef.current = term; fitAddonRef.current = fitAddon; term.writeln("šŸš€ Welcome to PyForge Bridge Terminal!"); term.writeln("šŸŒ‰ Connected to Server-Universal tools via Tenant Tool Bridge"); term.writeln("šŸ“ Each user gets their own isolated workspace with git configuration."); term.writeln(""); term.writeln("šŸ”§ Session Management Commands:"); term.writeln(" \x1b[38;5;244msession-create\x1b[0m - Create new isolated session"); term.writeln(" \x1b[38;5;244msession-info\x1b[0m - Show all active sessions"); term.writeln(" \x1b[38;5;244mgit-config-set\x1b[0m - Set git configuration for current session"); term.writeln(" \x1b[38;5;244mbridge-test\x1b[0m - Test bridge connectivity"); term.writeln(""); term.writeln("šŸ› ļø Tool Commands:"); term.writeln(" \x1b[38;5;244mbash <command>\x1b[0m - Execute bash command via bridge"); term.writeln(" \x1b[38;5;244msearch <query>\x1b[0m - Search EGW writings database"); term.writeln(" \x1b[38;5;244madmin <command>\x1b[0m - Execute admin command (requires password)"); term.writeln(""); term.writeln("šŸ’» All commands are executed within your isolated workspace."); term.writeln("🌐 Internet connectivity is enabled for all sessions."); term.writeln(""); if (currentSession) { term.writeln(`šŸ·ļø Current Session: ${currentSession.id.substring(0, 8)}... (${currentSession.status})`); term.writeln(`šŸ“ Workspace: ${currentSession.workspace}`); term.writeln(`šŸ”§ Git Config: ${currentSession.gitConfig.name ? 'āœ… Configured' : 'āŒ Not configured'}`); term.writeln(""); } else { term.writeln("āŒ No active session. Use 'session-create' to start."); term.writeln(""); } // Show bridge status if available if (bridgeStatus) { term.writeln(`šŸŒ‰ Bridge Status: ${bridgeStatus.status}`); term.writeln(`šŸ“” Server Universal: ${bridgeStatus.serverUniversalConnected ? 'āœ… Connected' : 'āŒ Disconnected'}`); term.writeln(""); } term.write(getPrompt(cwd)); let currentLine = ''; const executeCommand = async (command: string) => { if (!command.trim()) { term.write(getPrompt(cwd)); return; } if (commandHistory.current[commandHistory.current.length - 1] !== command) { commandHistory.current.push(command); } historyIndex.current = commandHistory.current.length; // Handle special session commands if (command === 'session-create') { term.write('\r\n'); handleCreateSession(); term.write(getPrompt(cwd)); return; } if (command === 'session-info') { term.write('\r\n'); handleShowSessions(); term.write(getPrompt(cwd)); return; } if (command === 'git-config-set') { term.write('\r\n'); handleUpdateGitConfig(); term.write(getPrompt(cwd)); return; } if (command === 'bridge-test') { term.write('\r\n'); handleTestBridge(); term.write(getPrompt(cwd)); return; } // Handle bridge commands if (!currentSession) { term.write('\r\nāŒ No active session. Use "session-create" to start.\r\n'); term.write(getPrompt(cwd)); return; } // Parse command type const parts = command.trim().split(' '); const cmd = parts[0]; if (cmd === 'bash' && parts.length > 1) { const bashCommand = parts.slice(1).join(' '); term.write('\r\n'); await handleBridgeCommand(bashCommand, currentSession.id); term.write(getPrompt(cwd)); return; } if (cmd === 'search' && parts.length > 1) { const searchQuery = parts.slice(1).join(' '); term.write('\r\n'); await handleDatabaseCommand(searchQuery, currentSession.id); term.write(getPrompt(cwd)); return; } if (cmd === 'admin' && parts.length > 1) { const adminCommand = parts.slice(1).join(' '); term.write('\r\n'); const result = await toolBridge.executeTool(currentSession.id, 'admin_local_server', { command: adminCommand, adminPassword: process.env.ADMIN_PASSWORD || 'admin18401844' }); if (result.success) { if (result.stdout) termWrite(`${result.stdout}\r\n`); if (result.stderr) termWrite(`āš ļø ${result.stderr}\r\n`); } else { termWrite(`āŒ Admin command failed: ${result.message}\r\n`); } term.write(getPrompt(cwd)); return; } // Default: treat as bash command term.write('\r\n'); await handleBridgeCommand(command, currentSession.id); term.write(getPrompt(cwd)); }; const keyListener = term.onKey(({ key, domEvent }) => { const printable = !domEvent.altKey && !domEvent.ctrlKey && !domEvent.metaKey; if (domEvent.key === 'Enter') { term.write('\r\n'); executeCommand(currentLine); currentLine = ''; } else if (domEvent.key === 'Backspace') { if (currentLine.length > 0) { term.write('\b \b'); currentLine = currentLine.slice(0, -1); } } else if (domEvent.key === 'ArrowUp') { if(historyIndex.current > 0) { historyIndex.current--; const cmd = commandHistory.current[historyIndex.current]; term.write(`\x1b[2K\r${getPrompt(cwd).trim()}${cmd}`); currentLine = cmd; } } else if (domEvent.key === 'ArrowDown') { if(historyIndex.current < commandHistory.current.length - 1) { historyIndex.current++; const cmd = commandHistory.current[historyIndex.current]; term.write(`\x1b[2K\r${getPrompt(cwd).trim()}${cmd}`); currentLine = cmd; } else { historyIndex.current = commandHistory.current.length; term.write(`\x1b[2K\r${getPrompt(cwd).trim()}`); currentLine = ''; } } else if (printable) { currentLine += key; term.write(key); } }); const resizeObserver = new ResizeObserver(() => fitAddon.fit()); resizeObserver.observe(termElRef.current); return () => { keyListener.dispose(); resizeObserver.disconnect(); }; }, [activeTab, cwd, handleCreateSession, handleShowSessions, handleUpdateGitConfig, handleTestBridge, handleBridgeCommand, handleDatabaseCommand, currentSession, toolBridge, bridgeStatus]); useEffect(() => { if (activeTab === 'terminal') { setTimeout(() => { fitAddonRef.current?.fit(); termRef.current?.focus(); }, 10); } }, [activeTab]); const TabButton: React.FC<{ name: string; tabId: Tab; }> = ({ name, tabId }) => ( <button onClick={() => setActiveTab(tabId)} style={{...styles.tabButton, ...(activeTab === tabId ? styles.activeTabButton : {})}} >{name}</button> ); return ( <div style={styles.container}> <div style={styles.tabHeader}> <TabButton name="Terminal" tabId="terminal" /> <TabButton name="Console" tabId="console" /> <TabButton name="Session" tabId="session" /> </div> <div style={styles.tabContent}> <div style={{...styles.tabPanel, display: activeTab === 'terminal' ? 'block' : 'none' }}> <div ref={termElRef} style={{width: '100%', height: '100%'}} /> </div> <div style={{...styles.tabPanel, display: activeTab === 'console' ? 'block' : 'none' }}> <Console output={terminalOutput} onClear={() => setTerminalOutput(['Console cleared.'])} /> </div> <div style={{...styles.tabPanel, display: activeTab === 'session' ? 'block' : 'none' }}> <div style={styles.sessionPanel}> <h3>Session Configuration</h3> <div style={styles.currentSessionInfo}> {currentSession ? ( <div> <p><strong>Current Session:</strong> {currentSession.id.substring(0, 8)}...</p> <p><strong>Status:</strong> {currentSession.status}</p> <p><strong>Workspace:</strong> {currentSession.workspace}</p> <p><strong>Git Configured:</strong> {currentSession.gitConfig.name ? 'āœ… Yes' : 'āŒ No'}</p> <p><strong>Bridge Status:</strong> {bridgeStatus?.status || 'Unknown'}</p> </div> ) : ( <p><strong>No active session</strong> - Create one to start working</p> )} </div> <div style={styles.configSection}> <h4>Git Configuration</h4> <input type="text" placeholder="Git User Name" value={gitConfig.name} onChange={(e) => setGitConfig(prev => ({ ...prev, name: e.target.value }))} style={styles.input} /> <input type="email" placeholder="Git User Email" value={gitConfig.email} onChange={(e) => setGitConfig(prev => ({ ...prev, email: e.target.value }))} style={styles.input} /> <input type="password" placeholder="GitHub Personal Access Token" value={gitConfig.pat} onChange={(e) => setGitConfig(prev => ({ ...prev, pat: e.target.value }))} style={styles.input} /> <div style={styles.buttonGroup}> <button onClick={handleCreateSession} style={styles.button}> Create New Session </button> <button onClick={handleUpdateGitConfig} style={styles.button} disabled={!currentSession}> Update Git Config </button> <button onClick={handleShowSessions} style={styles.button}> Show All Sessions </button> <button onClick={handleTestBridge} style={styles.button}> Test Bridge </button> </div> </div> </div> </div> </div> </div> ); }; const styles: { [key: string]: React.CSSProperties } = { container: { height: '100%', width: '100%', display: 'flex', flexDirection: 'column', backgroundColor: 'var(--background-secondary)' }, tabHeader: { display: 'flex', flexShrink: 0 }, tabButton: { background: 'var(--background-tertiary)', color: 'var(--text-secondary)', border: 'none', padding: '8px 12px', cursor: 'pointer', borderBottom: '1px solid var(--border-color)' }, activeTabButton: { background: 'var(--background-primary)', color: 'var(--text-primary)', borderBottom: '1px solid transparent' }, tabContent: { flex: 1, overflow: 'hidden', backgroundColor: 'var(--background-primary)' }, tabPanel: { width: '100%', height: '100%', padding: '5px' }, sessionPanel: { padding: '20px', height: '100%', overflow: 'auto' }, currentSessionInfo: { marginBottom: '20px', padding: '15px', backgroundColor: 'var(--background-secondary)', borderRadius: '8px' }, configSection: { marginBottom: '20px' }, input: { width: '100%', padding: '10px', margin: '5px 0', border: '1px solid var(--border-color)', borderRadius: '4px', backgroundColor: 'var(--background-primary)', color: 'var(--text-primary)' }, buttonGroup: { display: 'flex', gap: '10px', marginTop: '15px', flexWrap: 'wrap' }, button: { padding: '10px 15px', backgroundColor: 'var(--accent-primary)', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '14px' } }; export default TerminalBridge;

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/pythondev-pro/egw_writings_mcp_server'

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