Skip to main content
Glama
Terminal-MultiTenant.tsx•17.3 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'; type Tab = 'terminal' | 'console' | 'session'; interface Session { id: string; status: 'active' | 'idle' | 'expired'; workspaceDir: string; hasGitConfig: boolean; createdAt: string; lastActivity: string; } const CWD_MARKER = "CWD_MARKER_EXEC_FINISH"; export const TerminalMultiTenant: React.FC<{ terminalOutput: string[]; setTerminalOutput: React.Dispatch<React.SetStateAction<string[]>>; currentSession: Session | null; onSessionCreate: (gitConfig?: { name?: string; email?: string; pat?: string }) => void; onSessionUpdate: (sessionId: string, gitConfig: { name?: string; email?: string; pat?: string }) => void; }> = ({ terminalOutput, setTerminalOutput, currentSession, onSessionCreate, onSessionUpdate }) => { 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 { call: runCommand, call: createSession, call: updateSessionGit, call: destroySession, isPending } = useTool('run_bash_command'); const { call: getSessionInfo } = useTool('session_info'); const termWrite = (text: string) => termRef.current?.write(text.replace(/\n/g, '\r\n')); const getPrompt = (path: string) => `\r\n\x1b[1;34mpyforge\x1b[0m:\x1b[1;32m${path}\x1b[0m$ `; // Create new session const handleCreateSession = useCallback(async () => { try { const result = await createSession('session_create', { gitName: gitConfig.name || undefined, gitEmail: gitConfig.email || undefined, gitPat: gitConfig.pat || undefined }); const response = JSON.parse(result?.content?.[0]?.text || '{}'); if (response.success) { termWrite(`āœ… Session created: ${response.sessionId.substring(0, 8)}...\r\n`); termWrite(`šŸ“ Workspace: ${response.workspaceDir}\r\n`); setCwd('/workspace'); } else { termWrite(`āŒ Failed to create session: ${response.error}\r\n`); } } catch (error) { termWrite(`āŒ Error creating session: ${error.message}\r\n`); } }, [gitConfig, createSession]); // Update session git config const handleUpdateGitConfig = useCallback(async () => { if (!currentSession) { termWrite(`āŒ No active session. Create a session first.\r\n`); return; } try { const result = await updateSessionGit('session_update_git', { sessionId: currentSession.id, gitName: gitConfig.name || undefined, gitEmail: gitConfig.email || undefined, gitPat: gitConfig.pat || undefined }); const response = JSON.parse(result?.content?.[0]?.text || '{}'); if (response.success) { termWrite(`āœ… Git configuration updated for session ${currentSession.id.substring(0, 8)}...\r\n`); } else { termWrite(`āŒ Failed to update git config: ${response.error}\r\n`); } } catch (error) { termWrite(`āŒ Error updating git config: ${error.message}\r\n`); } }, [currentSession, gitConfig, updateSessionGit]); // Show session info const handleShowSessions = useCallback(async () => { try { const result = await getSessionInfo('session_info', {}); const response = JSON.parse(result?.content?.[0]?.text || '{}'); if (response.success) { termWrite(`šŸ“Š Session Overview:\r\n`); termWrite(` Total Sessions: ${response.info.totalSessions}\r\n`); termWrite(` Active Sessions: ${response.info.activeSessions}\r\n\r\n`); response.info.sessions.forEach((session: any) => { termWrite(` šŸ·ļø ${session.id} - ${session.status} (${session.age})\r\n`); }); } else { termWrite(`āŒ Failed to get session info: ${response.error}\r\n`); } } catch (error) { termWrite(`āŒ Error getting session info: ${error.message}\r\n`); } }, [getSessionInfo]); 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 Multi-Tenant Bash Terminal!"); 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(""); term.writeln("šŸ’» Regular bash commands work normally in your session 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.workspaceDir}`); term.writeln(`šŸ”§ Git Config: ${currentSession.hasGitConfig ? 'āœ… Configured' : 'āŒ Not configured'}`); term.writeln(""); } else { term.writeln("āŒ No active session. Use 'session-create' to start."); 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; } // Regular bash command execution if (!currentSession) { term.write('\r\nāŒ No active session. Use "session-create" to start.\r\n'); term.write(getPrompt(cwd)); return; } try { const result = await runCommand('run_bash_command', { sessionId: currentSession.id, command, cwd }); let newCwd = cwd; let output = result?.content?.[0]?.type === 'text' ? result.content[0].text : 'Command execution failed.'; const cwdMarkerIndex = output.lastIndexOf(CWD_MARKER); if (cwdMarkerIndex !== -1) { const parts = output.substring(cwdMarkerIndex).split(':'); newCwd = parts.slice(1).join(':').trim(); output = output.substring(0, cwdMarkerIndex).trimEnd(); } termWrite(output); setCwd(newCwd); } catch (error) { termWrite(`āŒ Command execution failed: ${error.message}\r\n`); } term.write(getPrompt(cwd)); }; const keyListener = term.onKey(({ key, domEvent }) => { if (isPending) return; 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(); }; }, [isPending, cwd, runCommand, currentSession, handleCreateSession, handleShowSessions, handleUpdateGitConfig]); 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.workspaceDir}</p> <p><strong>Git Configured:</strong> {currentSession.hasGitConfig ? 'āœ… Yes' : 'āŒ No'}</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> </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' }, button: { padding: '10px 15px', backgroundColor: 'var(--accent-primary)', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '14px' } }; export default TerminalMultiTenant;

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