Skip to content

Add standalone OpenGenerativeUI MCP Server#18

Merged
GeneralJerel merged 4 commits intomainfrom
feat/mcp-server
Mar 20, 2026
Merged

Add standalone OpenGenerativeUI MCP Server#18
GeneralJerel merged 4 commits intomainfrom
feat/mcp-server

Conversation

@GeneralJerel
Copy link
Collaborator

@GeneralJerel GeneralJerel commented Mar 20, 2026

Summary

Introduces a fully standalone, independently deployable MCP server that exposes OpenGenerativeUI's design system, skills, and renderer capabilities.

Key Features

  • Standalone Packageopen-generative-ui-mcp can be deployed independently
  • MCP Toolsassemble_document wraps HTML with design system CSS and bridge JS
  • MCP Resourcesskills://list and skills://{name} for browsing skill instructions
  • MCP Prompts — Pre-composed prompts: create_widget, create_svg_diagram, create_visualization
  • Production Ready — Docker support, configuration options, comprehensive documentation
  • Zero Demo Impact — Main app remains untouched; fully isolated

Files Added

apps/mcp/ — Complete MCP server package

  • src/ — Server implementation (index.ts, server.ts, skills.ts, renderer.ts)
  • skills/ — Own copies of skill instruction files
  • README.md — Comprehensive deployment and API documentation
  • Dockerfile — Multi-stage production container
  • Configuration files (.env.example, tsconfig.json, etc.)

Usage

Development

cd apps/mcp && pnpm dev
# → MCP server running on http://localhost:3100/mcp

Production (Docker)

docker build -t open-generative-ui-mcp .
docker run -p 3100:3100 open-generative-ui-mcp

Connect from Claude Code

Add to .mcp.json:

{
  "openGenerativeUI": {
    "url": "http://localhost:3100/mcp"
  }
}

Status

- Fully self-contained MCP server package (open-generative-ui-mcp)
- Exposes design system, skills, and renderer as MCP capabilities
- Includes three MCP tools/resources/prompts for generative UI
- Production-ready with Docker, configuration, and documentation
- Can be deployed independently without demo app dependency
- Complete README with deployment guides and API reference
Copy link
Collaborator Author

@GeneralJerel GeneralJerel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall the implementation is clean and well-structured — nice separation of concerns across the 4 source files. A few issues to address before merging, noted inline.


export function loadSkill(name: string): string {
return readFileSync(resolve(SKILLS_DIR, `${name}.txt`), "utf-8");
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: Path traversal vulnerability

name comes from MCP resource URI parameters and is passed directly into resolve(). A crafted name like ../../etc/passwd (with .txt appended) could read files outside the skills directory.

Suggest validating the resolved path stays within SKILLS_DIR:

export function loadSkill(name: string): string {
  const resolved = resolve(SKILLS_DIR, `${name}.txt`);
  if (!resolved.startsWith(resolve(SKILLS_DIR) + "/")) {
    throw new Error(`Invalid skill name: ${name}`);
  }
  return readFileSync(resolved, "utf-8");
}


# Install dependencies
RUN pnpm install --prod --frozen-lockfile

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: --prod flag breaks the build step

--prod skips devDependencies, but tsc (used on line 23 via pnpm build) is listed under devDependencies. This Docker build will fail at the RUN pnpm build step.

Should be:

RUN pnpm install --frozen-lockfile

Or split into two stages — full install for build, then prune to prod for the runtime image.

const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(",") || ["*"];

const server = createMcpServer();
const transport = new WebStandardStreamableHTTPServerTransport();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue: Single shared transport instance

The WebStandardStreamableHTTPServerTransport is instantiated once and shared across all HTTP requests. MCP's streamable HTTP transport is typically per-session — if two clients connect simultaneously, they may experience session conflicts.

Worth verifying whether the SDK handles concurrent sessions internally, or if you need to create a transport per request (or per session ID).

.DS_Store
.turbo/
.venv/
pnpm-lock.yaml
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conflict with Dockerfile: The .gitignore excludes pnpm-lock.yaml, but the Dockerfile (line 11) copies it for --frozen-lockfile install. The Docker build will fail unless the lockfile exists at build time.

Either remove this line (and commit the lockfile) or update the Dockerfile to not rely on --frozen-lockfile.

@@ -0,0 +1,350 @@
// OpenGenerativeUI Design System CSS and Bridge JS
// Forked from apps/app/src/components/generative-ui/widget-renderer.tsx

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says this is forked from widget-renderer.tsx — that's ~290 lines of duplicated CSS. If the design system evolves, both files need manual sync. Consider extracting the shared CSS to a common file or adding a more visible warning about keeping them in sync.

- Validate resolved path stays within SKILLS_DIR to prevent traversal
- Remove --prod from Dockerfile install so tsc is available for build
- Remove pnpm-lock.yaml from .gitignore so Dockerfile --frozen-lockfile works
- Create per-session transport instances instead of sharing one globally
- Add sync warning comment on forked renderer CSS
Claude Desktop uses stdio, not HTTP. Adds src/stdio.ts as a
separate entry point alongside the existing HTTP server.
@GeneralJerel GeneralJerel marked this pull request as ready for review March 20, 2026 16:17
- Add MCP Server section to root README with Claude Desktop and
  Claude Code setup instructions
- Update apps/mcp README with stdio usage for Claude Desktop
- Update project structure to include stdio.ts entry point
Copy link
Collaborator Author

@GeneralJerel GeneralJerel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Notes

Overall this is clean and well-structured — good separation of concerns, path traversal protection, per-session transports, and dual transport support. Build passes. A few things to consider:

Issues

  1. Dockerfile won't build from apps/mcp/ contextDockerfile:5 copies pnpm-lock.yaml, but the lockfile lives at the repo root (this is a workspace package). Running docker build -t open-generative-ui-mcp . from apps/mcp/ will fail. Either the build context needs to be the repo root, or the install strategy needs to change.

  2. Session map has no TTL or size limitindex.ts:11 sessions Map grows with each new connection and only cleans up on explicit transport close. No eviction for abandoned sessions. Fine for dev, but could leak memory under sustained use.

  3. LOG_LEVEL in .env.example is never read — Declared in the example env but not referenced anywhere in the code. Will confuse users trying to configure logging.

Minor observations

  • SKILLS_DIR from env resolves relative to CWD, but the code default resolves relative to __dirname. These behave differently — worth a note in the README or .env.example.
  • unsafe-eval in the CSP (renderer.ts:327) is a deliberate tradeoff for generated widgets, but worth calling out in docs.
  • The forked CSS in renderer.ts (~350 lines from widget-renderer.tsx) has the sync warning comment which is good — extracting to a shared location would prevent drift long-term.

Verdict

The Dockerfile issue is the only thing that blocks a documented workflow. The rest are non-blocking follow-ups. 👍

@GeneralJerel GeneralJerel merged commit fada403 into main Mar 20, 2026
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant