diff --git a/.env.example b/.env.example deleted file mode 100644 index d96e1ea..0000000 --- a/.env.example +++ /dev/null @@ -1,10 +0,0 @@ -# FastApps Configuration -# Copy this file to .env and configure your settings - -# Deployment Server URL -# Override the default deployment server URL -# Default: https://deploy.fastapps.org -# FASTAPPS_DEPLOY_URL=https://your-custom-deploy-server.com - -# Development Settings -# Add your development-specific environment variables here diff --git a/README.md b/README.md index ad3a1fe..094aa1b 100644 --- a/README.md +++ b/README.md @@ -27,59 +27,65 @@ ## Quick Start +We recommend installing FastApp with [uv](https://docs.astral.sh/uv/): + ```bash -# 0. Set virtual environment (recommended) -python -m venv .venv -source .venv/bin/activate # Mac/Linux -.venv\Scripts\Activate.ps1 # Windows PowerShell +uv pip install fastmcp +``` -# 1. Install -pip install fastapps -# Or with uv (faster, modern alternative): -# uv pip install fastapps +For full installation instructions, including verification, upgrading from the official MCPSDK, and developer setup, see the Installation Guide. -# 2. Create project (includes example widget + auto npm install) -fastapps init my-app +then, you can quickstart by running commands below : -# 3. Run +```bash +fastapps init my-app cd my-app -fastapps dev +fastapp dev ``` -That's it! Your example widget is now running at a public URL. -On first run, you'll need an [ngrok auth token](https://dashboard.ngrok.com/get-started/your-authtoken) (free). + +That's it! You'll gonna see an image with a public url. You can test the server with following guides. + +![alt text](image.png) + +The public url is one-time, generated with [cloudflare tunnel](https://github.com/cloudflare/cloudflared). ## Test App +MCP server is available at `/mcp` endpoint of fastapps server. \ +Example : https://your-public-url.trycloudflare.com/mcp + **Option A: Test on MCPJam Inspector** -Add your public URL + /mcp to ChatGPT: -Example: https://xyz.ngrok-free.app/mcp +Add your public URL + /mcp to ChatGPT. + ```bash npx @mcpjam/inspector@latest ``` **Option B: Test on ChatGPT** -Add your public URL + /mcp to ChatGPT's "Settings > Connectors": -Example: https://xyz.ngrok-free.app/mcp - +Add your public URL + /mcp to ChatGPT's `"Settings > Connectors"` . ## Creating More Widgets ```bash -fastapps create another-widget +fastapps create additional-widget ``` ### Editing Your Widget -**You only need to edit these 2 files:** +**You'll only need to edit these 2 folders:** -#### `server/tools/my_widget_tool.py` - Backend Logic +#### `server/tools/` -```python +This folder contains backend `.py` files, where you define conditions & server logics for the app. + +Example : +```python +### my_widget_tool.py from fastapps import BaseWidget, Field, ConfigDict from pydantic import BaseModel from typing import Dict, Any @@ -108,9 +114,14 @@ class MyWidgetTool(BaseWidget): } ``` -#### `widgets/my-widget/index.jsx` - Frontend UI +#### `widgets/` - Frontend UI + +The folder contains frontend component codes that will show up on the app screen according to the rules you've define with python codes above. + +Apps in GPT components are react components - FastApps follows it. You can custom compoenents as you wish. ```jsx +// my-widget/index.jsx import React from 'react'; import { useWidgetProps } from 'fastapps'; diff --git a/docs/01-INTRO.md b/docs/01-INTRO.md deleted file mode 100644 index f56c0aa..0000000 --- a/docs/01-INTRO.md +++ /dev/null @@ -1,334 +0,0 @@ -# Introduction to FastApps - -Welcome to FastApps - a zero-boilerplate framework for building interactive ChatGPT widgets powered by the Apps SDK! - -## What is FastApps? - -FastApps is a Python framework that eliminates the complexity of building Apps SDK widgets for ChatGPT. It handles all the MCP protocol boilerplate, auto-discovery, and build configuration so you can focus on writing your widget logic and UI. - -## The Problem: MCP is Powerful but Complex - -OpenAI's Apps SDK lets you build rich, interactive widgets for ChatGPT using the Model Context Protocol (MCP). But the manual setup is extensive: - -### Building a Widget with Raw MCP -```typescript -// 1. Manually register HTML resources with specific mime types -server.registerResource( - "widget-html", - "ui://widget/my-widget.html", - {}, - async () => ({ - contents: [{ - uri: "ui://widget/my-widget.html", - mimeType: "text/html+skybridge", // Must be exact - text: ` -
- - - `, - _meta: { - "openai/widgetCSP": { - connect_domains: [], - resource_domains: [] - } - } - }] - }) -); - -// 2. Manually wire tool metadata to resource URIs -server.registerTool( - "my-widget", - { - title: "My Widget", - inputSchema: { /* ... */ }, - _meta: { - "openai/outputTemplate": "ui://widget/my-widget.html", - "openai/widgetAccessible": true, - "openai/toolInvocation/invoking": "Loading...", - "openai/toolInvocation/invoked": "Done" - } - }, - async (input) => { - return { - content: [{ type: "text", text: "Success" }], - structuredContent: { /* data */ } - }; - } -); - -// 3. Manually build and bundle assets -// 4. Manually inject component mounting logic -// 5. Manually configure CSP policies -// 6. Manually set up server with proper protocol handlers -``` - -**That's a lot of boilerplate for every widget you build.** - -## The Solution: FastApps Automates Everything - -With FastApps, you write **just 2 files** and everything else is automatic: - -### Python Tool (Backend) -```python -from fastapps import BaseWidget, Field -from pydantic import BaseModel - -class MyInput(BaseModel): - name: str = Field(default="World") - -class MyWidgetTool(BaseWidget): - identifier = "my-widget" - title = "My Widget" - input_schema = MyInput - invoking = "Loading..." - invoked = "Done" - - widget_csp = { - "connect_domains": [], - "resource_domains": [] - } - - async def execute(self, input_data: MyInput): - return { - "message": f"Hello, {input_data.name}!" - } -``` - -### React Component (Frontend) -```jsx -import React from 'react'; -import { useWidgetProps } from 'fastapps'; - -export default function MyWidget() { - const props = useWidgetProps(); - - return ( -
-

{props?.message || 'Welcome!'}

-
- ); -} -``` - -**That's it! FastApps handles:** -- ✅ MCP server setup and protocol implementation -- ✅ Tool and resource registration -- ✅ HTML resource generation with proper mime types -- ✅ Metadata wiring (`openai/outputTemplate`, CSP, etc.) -- ✅ Asset building and bundling with Vite -- ✅ Component mounting logic injection -- ✅ Auto-discovery of widgets from your project structure - -## Key Features - -### Zero Boilerplate -No more manual MCP server setup: -- ✅ **No resource registration** - FastApps auto-generates HTML resources -- ✅ **No metadata wiring** - Tool metadata automatically configured -- ✅ **No build scripts** - Vite integration built-in -- ✅ **No mounting logic** - Component initialization injected automatically - -### Auto-Discovery -Drop a Python file in `server/tools/` and it's automatically discovered: - -```python -# server/tools/my_widget_tool.py -class MyWidgetTool(BaseWidget): - identifier = "my-widget" - # ... - -# That's it! No imports, no manual registration needed. -``` - -FastApps scans your `server/tools/` directory, finds all `BaseWidget` subclasses, and automatically: -- Registers them as MCP tools -- Creates HTML resources with correct mime types -- Wires metadata (`openai/outputTemplate`, CSP, etc.) -- Links them to your React components in `widgets/` - -### Fast Development Workflow -```bash -fastapps create mywidget # Generate boilerplate (2 files) -# Edit server/tools/mywidget_tool.py (your logic) -# Edit widgets/mywidget/index.jsx (your UI) -npm run build # Build assets -fastapps dev # Run server with public ngrok tunnel -``` - -That's it! No configuration files, no manual wiring, no separate ngrok setup. - -### Modern Stack -- **Backend**: Python + FastMCP (MCP protocol wrapper) -- **Frontend**: React + Vite (fast builds) -- **Protocol**: MCP (Model Context Protocol) -- **Type Safety**: Pydantic (Python) + TypeScript (React) -- **CLI**: Scaffolding commands for instant widget creation - -## Architecture Overview - -``` -┌─────────────────────────────────────────┐ -│ ChatGPT Interface │ -└─────────────────┬───────────────────────┘ - │ MCP Protocol - ▼ -┌─────────────────────────────────────────┐ -│ FastApps Framework │ -├─────────────────────────────────────────┤ -│ Python Backend (Your Tool) │ -│ ├── Input validation (Pydantic) │ -│ ├── Business logic │ -│ └── Data preparation │ -└─────────────────┬───────────────────────┘ - │ Props - ▼ -┌─────────────────────────────────────────┐ -│ React Frontend (Your Component) │ -│ ├── useWidgetProps() - Get data │ -│ ├── useWidgetState() - Manage state │ -│ └── Render UI │ -└─────────────────────────────────────────┘ -``` - -## Core Concepts - -### 1. Widget = Tool + Component - -Every widget consists of: -- **Tool** (Python): Backend logic, data fetching -- **Component** (React): UI rendering, interactivity - -### 2. Automatic Everything - -- **Discovery**: Tools automatically found in `server/tools/` -- **Registration**: No manual imports needed -- **Building**: Vite builds and bundles automatically -- **Mounting**: React mounting injected automatically - -### 3. Type Safety - -```python -# Python: Pydantic models -class MyInput(BaseModel): - name: str - age: int -``` - -```typescript -// TypeScript: Full type support -interface MyProps { - name: string; - age: number; -} -const props = useWidgetProps(); -``` - -## What Can You Build? - -### Data Visualizations -- Charts and graphs -- Maps and geospatial data -- Tables and grids -- Dashboards - -### Interactive Tools -- Calculators -- Converters -- Form builders -- Quiz apps - -### Content Displays -- Image galleries -- Video players -- Rich text editors -- Code highlighters - -### Integrations -- API data displays -- Database queries -- External service UIs -- Real-time updates - -## Comparison: Raw MCP vs FastApps - -| Aspect | Raw MCP (Apps SDK) | FastApps | -|--------|-------------------|----------| -| **Setup** | Manual server config, protocol handlers | Auto-configured | -| **Resource Registration** | Manual `registerResource()` calls | Auto-generated | -| **Tool Registration** | Manual `registerTool()` with metadata | Auto-discovered | -| **CSP Configuration** | Manual `_meta` object wiring | Simple `widget_csp` dict | -| **Asset Bundling** | Custom build scripts | Built-in Vite integration | -| **Component Mounting** | Manual injection logic | Auto-injected | -| **Files to Write** | 5+ (server, resource, tool, build, component) | **2** (tool.py, index.jsx) | -| **Lines of Boilerplate** | ~150+ per widget | **~0** | - -## How It Works (Under the Hood) - -When you run `python server/main.py`, FastApps: - -1. **Scans `server/tools/`** - Discovers all `BaseWidget` subclasses -2. **Auto-registers resources** - Creates HTML resources with `text/html+skybridge` mime type -3. **Auto-registers tools** - Wires tool metadata to resource URIs -4. **Configures CSP** - Converts your `widget_csp` dict to proper MCP metadata -5. **Serves MCP protocol** - Handles all protocol handshakes and requests -6. **Injects data** - Passes your `execute()` return value to React via `window.openai.toolOutput` - -All the MCP complexity is handled for you - you just write business logic and UI. - -## Design Philosophy - -### Abstraction Without Leakage -FastApps hides MCP complexity but doesn't limit what you can build: -- All Apps SDK features supported (CSP, state, tool access) -- You control the data and UI completely -- MCP protocol details abstracted away - -### Minimal API Surface -Learn once, build many: -- **1 base class**: `BaseWidget` (handles all MCP wiring) -- **3 React hooks**: `useWidgetProps`, `useWidgetState`, `useOpenAiGlobal` -- **3 CLI commands**: `init` (scaffold project), `create` (add widget), `dev` (run with ngrok) - -### Convention Over Configuration -Zero config files: -- Widget identifier must match folder name (enforced automatically) -- Tools in `server/tools/`, components in `widgets/` (auto-discovered) -- Build and serve with standard commands - -### Developer Experience First -- **Fast iterations** - Edit code, rebuild, reload -- **Clear errors** - Helpful validation messages -- **Type safety** - Pydantic + TypeScript -- **No surprises** - Explicit over implicit - -## Next Steps - -- [Quick Start Guide](./QUICKSTART.md) - Get started in 5 minutes -- [Building Widgets](./02-WIDGETS.md) - Create React components -- [Building Tools](./03-TOOLS.md) - Create Python backends -- [Managing State](./04-STATE.md) - Persistent widget state -- [API Reference](./API.md) - Complete API docs - -## Requirements - -- Python 3.11+ -- Node.js 18+ -- pip and npm - -## Philosophy - -> **"You should write your widget logic and UI, not MCP boilerplate."** - -FastApps is an abstraction layer over the Apps SDK. It handles all the protocol complexity so you can focus on building great widgets. - ---- - -**Ready to get started?** → [Quick Start Guide](./QUICKSTART.md) - diff --git a/docs/02-WIDGETS.md b/docs/02-WIDGETS.md deleted file mode 100644 index d2305b6..0000000 --- a/docs/02-WIDGETS.md +++ /dev/null @@ -1,542 +0,0 @@ -# Building Widgets (React Components) - -Learn how to create beautiful, interactive React components for your ChatGPT widgets. - -## What is a Widget? - -A widget is a React component that: -- Lives in `widgets//index.jsx` -- Receives props from your Python tool -- Renders in the ChatGPT interface -- Can be interactive and stateful - -## Basic Widget Structure - -```jsx -import React from 'react'; -import { useWidgetProps } from 'fastapps'; - -export default function MyWidget() { - // 1. Get data from Python backend - const props = useWidgetProps(); - - // 2. Render UI - return ( -
-

{props?.message || 'Loading...'}

-
- ); -} -``` - -**That's it!** Every widget follows this pattern. - -## Getting Data: useWidgetProps - -### Basic Usage - -```jsx -const props = useWidgetProps(); -// props = whatever your Python execute() returned -``` - -### With TypeScript - -```typescript -interface MyWidgetProps { - message: string; - count: number; - items: string[]; -} - -export default function MyWidget() { - const props = useWidgetProps(); - // props is now typed! - - return
{props?.message || 'Loading...'}
; -} -``` - -### Handling Missing Props - -```jsx -export default function MyWidget() { - const props = useWidgetProps(); - - // Use defaults with optional chaining - const message = props?.message || 'No message'; - - // Or conditional rendering - if (!props.data) { - return
No data available
; - } - - return
{props.data}
; -} -``` - -## Styling Widgets - -### Inline Styles (Recommended) - -```jsx -
- Content -
-``` - -**Why inline styles?** -- No build configuration needed -- Scoped to component -- Dynamic based on props -- Works everywhere - -### Responsive Design - -```jsx -
- Content -
-``` - -### Theme Support - -```jsx -import { useOpenAiGlobal } from 'fastapps'; - -export default function ThemedWidget() { - const theme = useOpenAiGlobal('theme'); - const isDark = theme === 'dark'; - - return ( -
- Content adapts to ChatGPT theme! -
- ); -} -``` - -## Common Patterns - -### Loading State - -```jsx -export default function MyWidget() { - const props = useWidgetProps(); - const [loading, setLoading] = React.useState(false); - - if (loading) { - return ( -
- Loading... -
- ); - } - - return
{props.data}
; -} -``` - -### Error Handling - -```jsx -export default function MyWidget() { - const props = useWidgetProps(); - - if (props.error) { - return ( -
- [Warning] Error: {props.error} -
- ); - } - - return
{props.data}
; -} -``` - -### Lists and Tables - -```jsx -export default function ListWidget() { - const props = useWidgetProps(); - - return ( -
-

Items

-
    - {props.items.map((item, index) => ( -
  • - {item.name} -
  • - ))} -
-
- ); -} -``` - -### Cards - -```jsx -export default function CardWidget() { - const props = useWidgetProps(); - - return ( -
-

{props.title}

-

{props.description}

-
- ); -} -``` - -### Buttons - -```jsx -export default function ButtonWidget() { - const props = useWidgetProps(); - const [state, setState] = useWidgetState({ clicked: false }); - - const handleClick = () => { - setState({ clicked: true }); - }; - - return ( - - ); -} -``` - -### Grids - -```jsx -export default function GridWidget() { - const props = useWidgetProps(); - - return ( -
- {props.items.map((item, i) => ( -
-

{item.title}

-

{item.description}

-
- ))} -
- ); -} -``` - -## Advanced Patterns - -### Conditional Rendering - -```jsx -export default function MyWidget() { - const props = useWidgetProps(); - - return ( -
- {props.showHeader && ( -
-

{props.title}

-
- )} - - {props.type === 'list' ? ( -
    - {props.items.map(item =>
  • {item.name}
  • )} -
- ) : ( -
-

{props.content}

-
- )} - - {props.showFooter && ( -
-

Footer content

-
- )} -
- ); -} -``` - -### Component Composition - -```jsx -// Sub-components -function Header({ title }) { - return

{title}

; -} - -function Card({ title, content }) { - return ( -
-

{title}

-

{content}

-
- ); -} - -// Main widget -export default function MyWidget() { - const props = useWidgetProps(); - - return ( -
-
- -
- {props.cards.map(card => ( - - ))} -
-
- ); -} -``` - -### Custom Hooks - -```jsx -import { useWidgetProps } from 'fastapps'; -import React from 'react'; - -// Custom hook for derived data -function useFilteredItems(items, filter) { - return React.useMemo(() => { - return items.filter(item => item.category === filter); - }, [items, filter]); -} - -export default function FilterWidget() { - const props = useWidgetProps(); - const [filter, setFilter] = React.useState('all'); - - const filteredItems = useFilteredItems(props.items, filter); - - return ( -
- - - {filteredItems.map(item => ( -
{item.name}
- ))} -
- ); -} -``` - -## Performance Tips - -### 1. Use React.memo for Expensive Components - -```jsx -const ExpensiveCard = React.memo(({ data }) => { - // Expensive rendering logic - return
...
; -}); - -export default function MyWidget() { - const props = useWidgetProps(); - - return ( -
- {props.items.map(item => ( - - ))} -
- ); -} -``` - -### 2. Avoid Inline Functions in Loops - -```jsx -// Bad: Creates new function on every render -{items.map(item => ( - -))} - -// Good: Stable references -{items.map(item => ( - -))} -``` - -### 3. Use useMemo for Expensive Calculations - -```jsx -import React from 'react'; - -export default function MyWidget() { - const props = useWidgetProps(); - - const sortedItems = React.useMemo(() => { - return props.items.sort((a, b) => a.priority - b.priority); - }, [props.items]); - - return
{/* render sortedItems */}
; -} -``` - -## Debugging - -### Console Logging - -```jsx -export default function MyWidget() { - const props = useWidgetProps(); - - // Debug props - React.useEffect(() => { - console.log('Widget props:', props); - }, [props]); - - return
...
; -} -``` - -### Error Boundaries - -```jsx -class ErrorBoundary extends React.Component { - state = { hasError: false }; - - static getDerivedStateFromError(error) { - return { hasError: true }; - } - - render() { - if (this.state.hasError) { - return
Something went wrong
; - } - return this.props.children; - } -} - -export default function MyWidget() { - return ( - - {/* Your widget code */} - - ); -} -``` - -## Best Practices - -1. **Keep it simple** - One widget, one responsibility -2. **Use semantic HTML** - `
`, `
`, `