{"id":46527781,"url":"https://github.com/mplemay/gdansk","last_synced_at":"2026-05-01T02:00:44.178Z","repository":{"id":339864428,"uuid":"1158604548","full_name":"mplemay/gdansk","owner":"mplemay","description":"React Frontends for Python MCP Servers","archived":false,"fork":false,"pushed_at":"2026-04-30T18:16:22.000Z","size":1261,"stargazers_count":5,"open_issues_count":6,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-30T18:21:27.165Z","etag":null,"topics":["fastapi","mcp","python","react","web"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mplemay.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"agents.md","dco":null,"cla":null}},"created_at":"2026-02-15T16:40:03.000Z","updated_at":"2026-04-23T05:30:56.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mplemay/gdansk","commit_stats":null,"previous_names":["mplemay/gdansk"],"tags_count":22,"template":false,"template_full_name":null,"purl":"pkg:github/mplemay/gdansk","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mplemay%2Fgdansk","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mplemay%2Fgdansk/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mplemay%2Fgdansk/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mplemay%2Fgdansk/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mplemay","download_url":"https://codeload.github.com/mplemay/gdansk/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mplemay%2Fgdansk/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32482460,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-30T13:12:12.517Z","status":"online","status_checked_at":"2026-05-01T02:00:05.856Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["fastapi","mcp","python","react","web"],"created_at":"2026-03-06T21:16:27.038Z","updated_at":"2026-05-01T02:00:44.171Z","avatar_url":"https://github.com/mplemay.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Gdansk: React Frontends for Python MCP Servers\n\n\u003e [!WARNING]\n\u003e This project is currently in beta. The APIs are subject to change leading up to v1.0. The v1.0 release will\n\u003e coincide with the v2.0 release of the [python mcp sdk](https://github.com/modelcontextprotocol/python-sdk)\n\n## Installation\n\n```bash\nuv add gdansk\n```\n\n## Skill for Coding Agents\n\nIf you use coding agents such as Claude Code or Cursor, add the gdansk skills to your repository:\n\n```shell\nnpx skills add mplemay/gdansk\n```\n\nThen use:\n\n- `$use-gdansk` to bootstrap gdansk in a new repo or add another widget to an existing integration.\n- `$debug-gdansk` to diagnose widget path, bundling, render, and runtime failures in an existing gdansk setup.\n\n## Compatibility\n\n- Python: `gdansk` currently requires `\u003e=3.12,\u003c3.15`.\n- Frontend package: use an ESM package with `@gdansk/vite`, `vite`, `@vitejs/plugin-react`, `react`, `react-dom`,\n  and `@modelcontextprotocol/ext-apps`. Inertia page mode also needs `@inertiajs/react`.\n- Runtime tooling: gdansk starts the frontend through `uv run deno ...`. If you run frontend package scripts directly,\n  the published `@gdansk/vite` package currently declares Node `\u003e=22`.\n\n## Examples\n\n- **[FastAPI](examples/fastapi):** Mounting the MCP app inside an existing FastAPI service.\n- **[inertia](examples/inertia):** Ship-backed Inertia pages for FastAPI with `gdanskPages()`.\n- **[get-time](examples/get-time):** Small copyable widget example for first-time adoption in another repo.\n- **[production](examples/production):** Minimal production-rendered and hydrated widget example with a single tool.\n- **[shadcn](examples/shadcn):** Multi-tool todo app with `structured_output=True` and `shadcn/ui`.\n\n## Inertia Pages\n\n`Ship` can serve convention-driven Inertia pages directly: the first request returns an HTML shell, follow-up\nrequests use the Inertia JSON protocol, and production assets still come from `ship.assets`.\n\nPage mode is convention-driven. Put the root page at `app/page.tsx`, nested pages at `app/**/page.tsx`, and\nco-located layouts at `app/**/layout.tsx`. Render the root page with `page.render(\"/\")`; nested folders map to\nslash-delimited component ids like `page.render(\"dashboard/reports\")`.\n\nFor FastAPI, inject the page with `Depends(ship.page)` and run the frontend with `ship.lifespan(...)`. Call\n`ship.inertia(...)` only when you need non-default page settings such as a custom root id or explicit version.\n\n```python\ntype PageDependency = Annotated[\"InertiaPage\", Depends(ship.page)]\n```\n\nPair the backend with `gdanskPages()` in your frontend `vite.config.ts`:\n\n```ts\nimport react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vite\";\nimport { gdanskPages } from \"@gdansk/vite\";\n\nexport default defineConfig({\n  plugins: [gdanskPages({ refresh: true }), react()],\n});\n```\n\nFor a full FastAPI example with validation errors, flash messages, deferred props, once props, merge helpers, scroll\nprops, and fragment redirects, see\n[`examples/inertia`](examples/inertia).\n\nThe backend helper surface is now close to the official non-SSR Inertia protocol:\n\n- `prop(value)` creates a fluent prop builder.\n- `optional(value)`, `always(value)`, and `defer(value, group=...)` control eager vs partial/deferred loading.\n- `once(value, key=...)` and `page.share_once(...)` emit `onceProps` so the client can reuse previously loaded data.\n- `merge(value)` / `deep_merge(value, match_on=...)` and `prop(...).append(...)` / `.prepend(...)` emit merge metadata.\n- `scroll(...)` emits both merge metadata and `scrollProps` for infinite-scroll style payloads.\n- `page.encrypt_history(...)`, `page.clear_history()`, and `page.redirect(..., preserve_fragment=True)` control history\n  and redirect behavior.\n\n```python\nfrom gdansk import deep_merge, merge, once, prop, scroll\n\npage.share_once(sessionToken=load_session_token)\n\nreturn await page.render(\n    \"/\",\n    {\n        \"announcements\": merge(load_announcements()).append(match_on=\"id\"),\n        \"conversation\": deep_merge(load_conversation(), match_on=\"messages.id\"),\n        \"feed\": scroll(\n            load_feed(),\n            items_path=\"items\",\n            current_page_path=\"pagination.current\",\n            next_page_path=\"pagination.next\",\n            previous_page_path=\"pagination.previous\",\n            page_name=\"feed_page\",\n        ),\n        \"profile\": once(load_profile, key=\"shared-profile\"),\n        \"stats\": prop(load_stats).optional(),\n    },\n)\n```\n\n## Quick Start\n\nHere's a complete example showing how to build a simple greeting tool with a React UI:\n\n**Project Structure:**\n\n```text\nmy-mcp-server/\n├── server.py\n└── frontend/\n    ├── package.json\n    ├── vite.config.ts\n    └── widgets/\n        └── hello/\n            └── widget.tsx\n```\n\nThe `frontend` folder name is only an example. Pass any frontend package root to `Vite(...)`.\nThat frontend package owns its own `vite.config.ts`; import `@gdansk/vite` there alongside any framework plugins.\n\n**server.py:**\n\n```python\nfrom collections.abc import AsyncIterator\nfrom contextlib import asynccontextmanager\nfrom pathlib import Path\n\nimport uvicorn\nfrom mcp.server import MCPServer\nfrom mcp.types import TextContent\nfrom starlette.middleware.cors import CORSMiddleware\n\nfrom gdansk import Ship, Vite\n\nfrontend_path = Path(__file__).parent / \"frontend\"\nship = Ship(vite=Vite(frontend_path))\n\n\n@ship.widget(path=Path(\"hello/widget.tsx\"), name=\"greet\")\ndef greet(name: str) -\u003e list[TextContent]:\n    \"\"\"Greet someone by name.\"\"\"\n    return [TextContent(type=\"text\", text=f\"Hello, {name}!\")]\n\n\n@asynccontextmanager\nasync def lifespan(mcp: MCPServer) -\u003e AsyncIterator[None]:\n    async with ship.lifespan(mcp=mcp, watch=True):\n        yield\n\n\nmcp = MCPServer(name=\"Hello World Server\", lifespan=lifespan)\n\n\ndef main() -\u003e None:\n    app = mcp.streamable_http_app()\n    app.add_middleware(\n        CORSMiddleware,\n        allow_origins=[\"*\"],\n        allow_methods=[\"*\"],\n        allow_headers=[\"*\"],\n    )\n    app.mount(path=ship.assets_path, app=ship.assets)\n    uvicorn.run(app, port=3000)\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n**frontend/widgets/hello/widget.tsx:**\n\n```tsx\nimport { useApp } from \"@modelcontextprotocol/ext-apps/react\";\nimport { useState } from \"react\";\n\nexport default function App() {\n  const [name, setName] = useState(\"\");\n  const [greeting, setGreeting] = useState(\"\");\n\n  const { app, error } = useApp({\n    appInfo: { name: \"Greeter\", version: \"1.0.0\" },\n    capabilities: {},\n  });\n\n  if (error) return \u003cdiv\u003eError: {error.message}\u003c/div\u003e;\n  if (!app) return \u003cdiv\u003eConnecting...\u003c/div\u003e;\n\n  return (\n    \u003cmain\u003e\n      \u003ch2\u003eSay Hello\u003c/h2\u003e\n      \u003cinput\n        value={name}\n        onChange={(e) =\u003e setName(e.target.value)}\n        placeholder=\"Enter your name...\"\n      /\u003e\n      \u003cbutton\n        onClick={async () =\u003e {\n          const result = await app.callServerTool({\n            name: \"greet\",\n            arguments: { name },\n          });\n          const text = result.content?.find((c) =\u003e c.type === \"text\");\n          if (text \u0026\u0026 \"text\" in text) setGreeting(text.text);\n        }}\n      \u003e\n        Greet Me\n      \u003c/button\u003e\n      {greeting \u0026\u0026 \u003cp\u003e{greeting}\u003c/p\u003e}\n    \u003c/main\u003e\n  );\n}\n```\n\n**frontend/package.json:**\n\n```json\n{\n  \"name\": \"my-mcp-frontend\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@gdansk/vite\": \"^0.1.0\",\n    \"@modelcontextprotocol/ext-apps\": \"^1.5.0\",\n    \"@vitejs/plugin-react\": \"^6.0.1\",\n    \"react\": \"^19.2.5\",\n    \"react-dom\": \"^19.2.5\",\n    \"vite\": \"^8.0.8\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^19.2.14\",\n    \"@types/react-dom\": \"^19.2.3\"\n  }\n}\n```\n\n**frontend/vite.config.ts:**\n\n```ts\nimport react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vite\";\nimport gdansk from \"@gdansk/vite\";\n\nexport default defineConfig({\n  plugins: [gdansk({ refresh: true }), react()],\n});\n```\n\n`@gdansk/vite` now provides a default `@` alias that points at the frontend package root, so you only need a manual\nalias when you want `@` to resolve somewhere else. Use `refresh: true` to trigger full browser reloads when nearby\nPython or Jinja files change during development.\n\nFor widget-based MCP apps, `ship.lifespan(..., watch=...)` controls how the frontend is prepared:\n\n- **`watch=True`** — runs the Vite dev server in the background with React refresh; JS/CSS load from the Vite origin.\n- **`watch=False`** (default) — runs `vite build` on startup, then serves static hydration assets and the gdansk\n  manifest from `ship.assets`.\n- **`watch=None`** — skips Vite/Deno entirely and loads an existing `gdansk-manifest.json` under the assets directory.\n  Use this when assets are prebuilt (for example in CI) to avoid cold-start build cost.\n\nIf you need a non-default build output directory, keep the Vite plugin and Python runtime aligned. Widget sources\nalways live under `widgets/` at the frontend package root (`Vite(root=...)` / Vite `root`).\n\n```python\nship = Ship(\n    vite=Vite(\n        Path(__file__).parent / \"frontend\",\n        build_directory=\"public/ui\",\n    ),\n)\n```\n\n```ts\nexport default defineConfig({\n  plugins: [\n    gdansk({\n      buildDirectory: \"public/ui\",\n      refresh: true,\n    }),\n    react(),\n  ],\n});\n```\n\nProduction widgets load their hydration assets from `ship.assets_path`. Mount `ship.assets` at that path on the\npublic app; with the default settings this is `/dist`.\n\nThe default production output now mirrors Vite/Laravel conventions more closely:\n\n- standard Vite manifest: `dist/manifest.json`\n- gdansk runtime manifest: `dist/gdansk-manifest.json`\n- stable widget entries: `dist/\u003cwidget\u003e/client.js` and `dist/\u003cwidget\u003e/client.css`\n- shared hashed assets and chunks: `dist/assets/*`\n\nIf your MCP client renders widget HTML on a different origin, pass `base_url` to `Ship` so production asset URLs point\nback to your public app instead of the client host:\n\n```python\nship = Ship(vite=Vite(Path(__file__).parent / \"frontend\"), base_url=\"https://example.com\")\n```\n\nIf you want a different dev runtime host or port, configure both sides explicitly:\n\n```python\nfrom gdansk import Ship, Vite\n\nship = Ship(vite=Vite(Path(__file__).parent / \"frontend\", host=\"127.0.0.1\", port=14000))\n```\n\n```ts\nexport default defineConfig({\n  plugins: [gdansk({ host: \"127.0.0.1\", port: 14000, refresh: true }), react()],\n});\n```\n\nInstall the frontend package dependencies from `frontend/` after editing them:\n\n```bash\ncd frontend\nuv run deno install\n```\n\nGdansk mounts your default export into `#root` automatically and wraps it with `React.StrictMode`.\n\nRun the server with `uv run python server.py`, configure it in your MCP client (like Claude Desktop), and you'll have\nan interactive greeting tool ready to use.\n\n## Why Use Gdansk?\n\n1. **Python Backend, React Frontend** — Use familiar technologies you already know. Write your logic in Python with type\n   hints, build your UI in React/TypeScript. No need to learn a new framework-specific language.\n\n2. **Built for MCP** — Composes with `MCPServer` from the official Python SDK: register widget tools and HTML resources\n   via `Ship`, wire them in with `ship.lifespan(mcp=...)`, and integrate with Claude Desktop and other MCP clients.\n\n3. **Fast bundling with Rolldown** — The Rolldown bundler processes your TypeScript/JSX automatically. Hot-reload in\n   development mode means you see changes instantly without manual rebuilds.\n\n4. **Type-Safe** — Full type safety across the stack. Python type hints on the backend, TypeScript on the frontend, with\n   automatic type checking via ruff and TypeScript compiler.\n\n5. **Developer-Friendly** — Simple decorator API (`@ship.widget()`), automatic resource registration, dev mode on\n   `ship.lifespan(...)`, and comprehensive error messages. Get started in minutes, not hours.\n\n6. **Production Ready** — Comprehensive test suite covering Python 3.12+ across Linux, macOS, and Windows. Used in\n   production MCP servers with proven reliability.\n\n## Credits\n\nGdansk builds on the shoulders of giants:\n\n- **[Model Context Protocol](https://modelcontextprotocol.io/)** — Official MCP documentation\n- **[@modelcontextprotocol/ext-apps](https://www.npmjs.com/package/@modelcontextprotocol/ext-apps)** — React hooks for\n  MCP apps\n- **[Rolldown](https://rolldown.rs/)** — Fast JavaScript bundler\n- **[mcp/python-sdk](https://github.com/modelcontextprotocol/python-sdk)** — Python SDK for MCP server development\n- **[Deno](https://deno.com/)** — JavaScript/TypeScript runtime used by the embedded Deno tooling\n\nSpecial thanks to the Model Context Protocol team at Anthropic for creating the MCP standard and the\n`@modelcontextprotocol/ext-apps` package.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmplemay%2Fgdansk","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmplemay%2Fgdansk","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmplemay%2Fgdansk/lists"}