Skip to main content
Glama
App.tsx11 kB
import React, { useState, useEffect, useCallback, useRef } from 'react'; import type { PyodideInterface } from 'pyodide'; import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; import { PackageManager } from './components/PackageManager'; import { CodeEditor } from './components/CodeEditor'; import { Console } from './components/Console'; import { Spinner } from './components/Spinner'; import { ActivityBar, Activity } from './components/ActivityBar'; import { FileExplorer } from './components/FileExplorer'; import { Terminal } from './components/Terminal'; import { saveState, loadState, FileSystemState } from './lib/vfs'; import { PythonLogo, PlayIcon } from './components/icons'; declare global { interface Window { loadPyodide: (config: { indexURL: string }) => Promise<PyodideInterface>; } } const DEFAULT_CODE = `import os print("Hello from PyForge IDE!") print("Current directory contains:", os.listdir('.')) # Try creating a new file and writing to it! # with open("new_file.txt", "w") as f: # f.write("This file was created by a Python script.") # print("Current directory now contains:", os.listdir('.')) `; const DEFAULT_FS: FileSystemState = { '/main.py': { type: 'file', content: DEFAULT_CODE, } }; enum Status { LOADING, READY, INSTALLING, RUNNING, } const App: React.FC = () => { const [pyodide, setPyodide] = useState<PyodideInterface | null>(null); const [status, setStatus] = useState<Status>(Status.LOADING); const [consoleOutput, setConsoleOutput] = useState<string>(''); const [installedPackages, setInstalledPackages] = useState<string[]>(['numpy', 'micropip']); const [activeActivity, setActiveActivity] = useState<Activity>('files'); const [fsState, setFsState] = useState<FileSystemState>({}); const [activeFile, setActiveFile] = useState<string | null>(null); const [editorContent, setEditorContent] = useState<string>(''); const terminalRef = useRef<{ write: (data: string) => void }>(null); const syncFsToVfs = (py: PyodideInterface, path: string = '/') => { const newState: FileSystemState = {}; const traverse = (currentPath: string) => { const entries = py.FS.readdir(currentPath); for (const entry of entries) { if (entry === '.' || entry === '..') continue; const fullPath = `${currentPath === '/' ? '' : currentPath}/${entry}`; const stat = py.FS.stat(fullPath); if (py.FS.isDir(stat.mode)) { newState[fullPath] = { type: 'directory' }; traverse(fullPath); } else if (py.FS.isFile(stat.mode)) { try { const content = py.FS.readFile(fullPath, { encoding: 'utf8' }); newState[fullPath] = { type: 'file', content: content as string }; } catch (e) { console.warn(`Could not read file ${fullPath}:`, e); } } } }; traverse(path); if(Object.keys(fsState).length === 0 && Object.keys(newState).length > 0) { setFsState(newState); } }; const updateFsState = (updater: (prevState: FileSystemState) => FileSystemState) => { setFsState(prevState => { const newState = updater(prevState); saveState(newState); return newState; }); }; const addToConsole = useCallback((s: string) => { setConsoleOutput((prev) => prev + s); }, []); const clearConsole = useCallback(() => setConsoleOutput(''), []); useEffect(() => { const initPyodide = async () => { try { addToConsole('Initializing Pyodide runtime...\n'); const pyodideInstance = await window.loadPyodide({ indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.25.1/full/', }); const vfsState = loadState() || DEFAULT_FS; Object.entries(vfsState).forEach(([path, node]) => { if (node.type === 'directory') { pyodideInstance.FS.mkdirTree(path); } else { const dir = path.substring(0, path.lastIndexOf('/')); if (dir) { pyodideInstance.FS.mkdirTree(dir); } pyodideInstance.FS.writeFile(path, node.content || '', { encoding: 'utf8' }); } }); setFsState(vfsState); const firstFile = Object.keys(vfsState).find(k => vfsState[k].type === 'file'); if (firstFile) { setActiveFile(firstFile); setEditorContent(vfsState[firstFile].content || ''); } pyodideInstance.setStdout({ batched: (s: string) => addToConsole(s + '\n') }); pyodideInstance.setStderr({ batched: (s: string) => addToConsole(`[ERROR] ${s}\n`) }); addToConsole('Loading initial packages (numpy)...\n'); await pyodideInstance.loadPackage('numpy'); setPyodide(pyodideInstance); setStatus(Status.READY); addToConsole('✅ Pyodide is ready. You can now run Python code and install packages.\n'); } catch (error) { console.error('Failed to load Pyodide:', error); addToConsole(`[FATAL] Failed to initialize Pyodide runtime: ${error}\n`); } }; initPyodide(); }, [addToConsole]); useEffect(() => { const handleSave = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 's' && activeFile && pyodide) { e.preventDefault(); pyodide.FS.writeFile(activeFile, editorContent, { encoding: 'utf8' }); updateFsState(prev => ({ ...prev, [activeFile]: { type: 'file', content: editorContent } })); console.log(`Saved ${activeFile}`); } }; window.addEventListener('keydown', handleSave); return () => window.removeEventListener('keydown', handleSave); }, [activeFile, editorContent, pyodide]); const handleRunCode = async () => { if (!pyodide || status !== Status.READY || !activeFile) return; setStatus(Status.RUNNING); clearConsole(); addToConsole(`>>> Running ${activeFile}...\n`); try { const codeToRun = pyodide.FS.readFile(activeFile, { encoding: 'utf8' }); await pyodide.runPythonAsync(codeToRun as string); } catch (error) { console.error(error); addToConsole(`[EXECUTION ERROR] ${error}\n`); } finally { setStatus(Status.READY); } }; const handleInstallPackage = async (packageName: string) => { if (!pyodide || status !== Status.READY || !packageName.trim()) return; setStatus(Status.INSTALLING); const installMsg = `\n>>> Installing package: ${packageName}...\n`; terminalRef.current?.write(installMsg); try { await pyodide.loadPackage('micropip'); const micropip = pyodide.pyimport('micropip'); // Redirect micropip output to terminal pyodide.globals.set('print_to_terminal', (s: string) => terminalRef.current?.write(s + '\r\n')); await pyodide.runPythonAsync(` import micropip import sys from contextlib import redirect_stdout, redirect_stderr import io f = io.StringIO() with redirect_stdout(f), redirect_stderr(f): try: await micropip.install('${packageName}') except Exception as e: print(e) output = f.getvalue() print_to_terminal(output) `); setInstalledPackages((prev) => [...new Set([...prev, packageName.trim()])].sort()); terminalRef.current?.write(`✅ Successfully installed ${packageName}.\n`); } catch (error) { console.error(error); terminalRef.current?.write(`[INSTALLATION ERROR] Failed to install ${packageName}: ${error}\n`); } finally { setStatus(Status.READY); } }; const handleFileSelect = (path: string) => { const node = fsState[path]; if (node?.type === 'file') { setActiveFile(path); setEditorContent(node.content || ''); } }; if (status === Status.LOADING) { return ( <div className="flex flex-col items-center justify-center h-screen bg-primary font-sans"> <PythonLogo /> <h1 className="text-2xl font-bold mt-4 text-text-primary">PyForge IDE</h1> <p className="text-text-secondary mt-2">Initializing Python Runtime Environment...</p> <div className="mt-6"> <Spinner /> </div> <pre className="mt-8 text-xs text-left bg-secondary p-4 rounded-lg w-full max-w-xl h-48 overflow-y-auto font-mono text-text-secondary"> {consoleOutput} </pre> </div> ); } return ( <div className="h-screen w-screen flex font-sans bg-secondary"> <ActivityBar activeActivity={activeActivity} onActivityChange={setActiveActivity} /> <PanelGroup direction="horizontal" className="flex-grow"> <Panel defaultSize={20} minSize={15} maxSize={40}> <div className="h-full w-full bg-secondary p-2"> {activeActivity === 'files' && pyodide && ( <FileExplorer pyodide={pyodide} fsState={fsState} onStateChange={updateFsState} onFileSelect={handleFileSelect} activeFile={activeFile} /> )} {activeActivity === 'packages' && ( <PackageManager onInstall={handleInstallPackage} isInstalling={status === Status.INSTALLING} installedPackages={installedPackages} /> )} </div> </Panel> <PanelResizeHandle /> <Panel minSize={30}> <PanelGroup direction="vertical"> <Panel minSize={30}> <header className="flex items-center justify-between px-4 py-2 bg-secondary border-b border-border-color flex-shrink-0"> <div className="text-sm text-text-secondary">{activeFile || 'No file selected'}</div> <button onClick={handleRunCode} disabled={status !== Status.READY || !activeFile} className="flex items-center gap-2 px-4 py-1 bg-success text-white font-semibold rounded-md hover:bg-green-600 transition-colors disabled:bg-gray-500 disabled:cursor-not-allowed" > {status === Status.RUNNING ? ( <> <Spinner /> Running... </> ) : ( <> <PlayIcon /> Run </> )} </button> </header> <CodeEditor value={editorContent} onValueChange={setEditorContent} /> </Panel> <PanelResizeHandle /> <Panel defaultSize={35} minSize={15}> <Terminal pyodide={pyodide!} onInstallPackage={handleInstallPackage} terminalRef={terminalRef} consoleOutput={consoleOutput} clearConsole={clearConsole} /> </Panel> </PanelGroup> </Panel> </PanelGroup> </div> ); }; export default App;

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