The web interface for ContextForge, the open source AI gateway that federates tools, agents, and APIs into one endpoint.
The backing service is a separate process in a separate repository (IBM/mcp-context-forge). This repository holds the BFF and client that sit in front of it. The table below also documents the API for naming reference, though it lives in a separate repo:
| Component | Lives in | Role |
|---|---|---|
| ContextForge API | separate repo | FastAPI service that owns auth and all business data |
| BFF | server/ |
Fastify app holding the session/CSRF boundary in front of the API |
| Client | src/ |
React SPA, served as static files by the BFF |
Throughout this README, "the API", "the BFF", and "the client" refer to those three. The browser only ever talks to the BFF, never directly to the API.
This UI targets ContextForge API v1.0.7, matching openapi.json committed at repo root.
- React 18 with TypeScript
- Vite: build tool and dev server
- React Router: client-side routing
- React Intl: internationalization (i18n)
- Tailwind CSS: utility-first styling
- shadcn/ui: component library
Running this in Docker instead? See DOCKER.md.
- Node.js 20+ and npm
npm installAll three components must be running for local dev. Beyond the roles above:
the BFF keeps the API's JWT off the browser (server/src/index.ts), and the
client is served same-origin by the BFF, so its requests are always relative
paths (src/api/client.ts).
Bring them up in this order:
-
Start the ContextForge API (terminal A). It is a separate service in its own clone, not part of this repository. Follow its own quick-start guide for first-time setup: IBM/mcp-context-forge#2503
cd /path/to/mcp-context-forge make dev # listens on :8000, matching .env.example's default below
(
make serveruns it in production mode on:4444instead.) Note whichever port yours ends up on; step 2 needs it. -
Configure and start the BFF (terminal B, from the repo root):
cp .env.example .env
Edit
.env:CONTEXTFORGE_URL: point it at whatever host:port the API is listening on from step 1 (.env.example's default is0.0.0.0:8000, which matchesmake dev; confirm against your actual run rather than assuming).COOKIE_SECURE=false: needed for local HTTP; the default (true) is for prod and silently drops the session cookie over plain HTTP.
Other values (
PORT,REDIS_URL,SESSION_TTL_SECONDS, etc.) have dev-safe defaults; see comments in.env.example.REDIS_URLis left unset, which falls back to an in-process store (no Redis process needed for local dev; state resets on restart).cd server npm install npm run dev # :3000, tsx watch, reads ../.env
-
Build the frontend for the BFF to serve, from the repo root:
npm install npm run build
This builds the SPA into
server/public/, which the already-running BFF serves directly. Re-runnpm run buildafter any frontend change; there's no HMR dev server wired to the BFF, so this build step is the loop for local iteration against the real backend. (npm run build:watchreruns it automatically on file changes.) -
Use it. Visit
http://localhost:3000/: redirects to/app/login(unauthed) or/app/(authed). The login form posts through the BFF, which holds the API's JWT server-side and hands the browser only an opaque session cookie.Default seeded admin:
admin@example.com/changeme(first login forces a password change unlessPASSWORD_CHANGE_ENFORCEMENT_ENABLED=falseis set in the API's.env).
npm run dev(plain Vite dev server at:5173, no BFF in front) still works for UI-only iteration, but/api/*calls need the BFF — it won't reach the ContextForge API on its own.
EADDRINUSEon:3000: staletsx watchprocess:lsof -ti:3000 | xargs kill, then restartnpm run devinserver/.- 401 mid-session: expected; the API token hard-expires per
TOKEN_EXPIRY(default 20 min). The BFF auto-revokes the session and redirects to login.
npm run buildBuilds the SPA into server/public/, for the BFF to serve.
npm run previewTypeScript types and fetch clients under src/generated/ come from openapi.json via Orval. That file is committed and pinned to API v1.0.7, not re-fetched at build time.
npm run generate # regenerate src/generated/ from ./openapi.jsonTo bump the API version, replace openapi.json with the new spec, update the version note above, then run npm run generate.
ESLint is configured with TypeScript support and Prettier integration.
# Check for linting errors
npm run lint
# Auto-fix linting errors
npm run lint:fixConfiguration: eslint.config.js
Prettier is configured for consistent code formatting.
# Format all files
npm run format
# Check formatting without changes
npm run format:checkConfiguration: .prettierrc
Key Settings:
- Trailing commas:
all(including function calls) - Semicolons:
true - Single quotes:
false(use double quotes) - Print width:
100
- Vitest: Fast unit test runner with jsdom environment
- React Testing Library: Component testing utilities
- MSW (Mock Service Worker): API mocking
# Run tests in watch mode
npm run test
# Run tests once (CI mode)
npm run test:run
# Run tests with UI
npm run test:ui
# Generate coverage report
npm run test:coveragesrc/
├── test/
│ ├── setup.ts # Global test setup (MSW, matchers, mocks)
│ ├── setup.d.ts # TypeScript declarations for jest-dom
│ ├── test-utils.tsx # Custom render with providers (I18nProvider)
│ └── mocks/
│ ├── server.ts # MSW server setup
│ └── handlers.ts # API request handlers
└── **/*.test.tsx # Test files (co-located with components)
Tests use React Testing Library with jest-dom matchers:
import { describe, it, expect } from "vitest";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "./test/test-utils";
import { MyComponent } from "./MyComponent";
describe("MyComponent", () => {
it("renders and handles user interaction", async () => {
const user = userEvent.setup();
renderWithProviders(<MyComponent />);
const button = screen.getByRole("button", { name: /click me/i });
await user.click(button);
expect(screen.getByText(/success/i)).toBeInTheDocument();
});
});Key Points:
- Use
renderWithProviders()instead ofrender()to wrap components with I18nProvider - Use
userEventfor simulating user interactions (more realistic thanfireEvent) - Use
screenqueries with accessible roles and names - MSW automatically mocks API requests defined in
src/test/mocks/handlers.ts
Add handlers to src/test/mocks/handlers.ts:
import { http, HttpResponse } from "msw";
export const handlers = [
http.get("/api/users", () => {
return HttpResponse.json([
{ id: 1, name: "John Doe" },
{ id: 2, name: "Jane Smith" },
]);
}),
http.post("/api/users", async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: 3, ...body }, { status: 201 });
}),
];Test-specific TypeScript configuration:
tsconfig.app.json: includesvitest/globalsand@testing-library/jest-domtypessrc/vitest.d.ts: global type declarations for test utilitiesvitest.config.ts: Vitest configuration with jsdom environment
End-to-end tests live in e2e/ and are written in TypeScript with
Playwright. They run against the Vite dev server and stub backend API calls
with page.route(), so no running ContextForge API is required.
npm run e2e:install # Install Playwright browsers (one-time)
npm run e2e # Headless run
npm run e2e:ui # Interactive UI mode
npm run e2e:debug # Playwright Inspector
npm run e2e:report # Open the last HTML reportSee e2e/README.md for layout, fixtures, and guidelines.
Tests and linting run automatically on pull requests via .github/workflows/client-lint-test.yml.
E2E tests run via .github/workflows/client-e2e.yml.
Workflow Steps:
- Install dependencies
- Run Prettier format check
- Run ESLint
- Run Vitest tests
Triggers:
- Push to
mainorepic/ui-rewritebranches - Pull requests to
mainorepic/ui-rewritebranches
contextforge-web-ui/
├── src/
│ ├── api/ # API client and types
│ ├── auth/ # Authentication context and hooks
│ ├── components/ # Reusable UI components
│ │ ├── layout/ # Layout components (Header, Sidebar, etc.)
│ │ └── ui/ # shadcn/ui components
│ ├── hooks/ # Custom React hooks
│ ├── i18n/ # Internationalization
│ │ └── locales/ # Translation files (en-US, es-ES, pt-BR)
│ ├── pages/ # Page components (Dashboard, Gateways, etc.)
│ ├── router/ # React Router configuration
│ ├── test/ # Test utilities and mocks
│ ├── App.tsx # Root component
│ └── main.tsx # Application entry point
├── public/ # Static assets
├── .prettierrc # Prettier configuration
├── .prettierignore # Prettier ignore patterns
├── eslint.config.js # ESLint configuration
├── vitest.config.ts # Vitest configuration
├── tsconfig.json # TypeScript base config
├── tsconfig.app.json # TypeScript app config
├── vite.config.ts # Vite configuration (builds to server/public/)
├── package.json # Dependencies and scripts
├── .env.example # Shared BFF config — copy to .env (see Getting Started)
├── .env.prod.example # Production-ready template — copy to .env
├── Dockerfile / docker-compose.yml / DOCKER.md # see DOCKER.md
└── server/ # BFF (Fastify): session/CSRF boundary in front of the API
├── src/
│ ├── index.ts # Entrypoint
│ ├── config.ts # Env-driven config
│ ├── plugins/ # cookie, redis, session, csrf, static
│ └── routes/ # auth/, proxy/ (catch-all to the API), sse/
├── public/ # Built SPA (npm run build output), served by BFF
└── package.json
| Script | Description |
|---|---|
npm run dev |
Start development server |
npm run build |
Build for production |
npm run generate |
Regenerate API types from openapi.json |
npm run preview |
Preview production build |
npm run lint |
Check for linting errors |
npm run lint:fix |
Auto-fix linting errors |
npm run format |
Format all files with Prettier |
npm run format:check |
Check formatting without changes |
npm run test |
Run tests in watch mode |
npm run test:run |
Run tests once (CI mode) |
npm run test:ui |
Run tests with UI |
npm run test:coverage |
Generate coverage report |
npm run e2e |
Run Playwright E2E tests |
npm run e2e:ui |
Playwright UI mode |
npm run e2e:debug |
Playwright Inspector |
npm run e2e:install |
Install Playwright browsers |
npm run e2e:report |
Open last Playwright report |
The app supports multiple languages via React Intl:
- English (en-US) (default)
- Spanish (es-ES)
- Portuguese (pt-BR)
Translation files are located in src/i18n/locales/.
- Add keys to
src/i18n/locales/{locale}/[domain].json - Use in components:
import { useIntl } from "react-intl";
function MyComponent() {
const intl = useIntl();
return <h1>{intl.formatMessage({ id: "navigation.dashboard" })}</h1>;
}Ensure TypeScript types are properly configured:
- Check
tsconfig.app.jsonincludes"types": ["vitest/globals", "@testing-library/jest-dom"] - Verify
src/vitest.d.tsexists with proper type references
- Verify handlers are defined in
src/test/mocks/handlers.ts - Check that paths match exactly (e.g.,
/app/auth/loginnot/api/auth/login) - Ensure MSW server is started in
src/test/setup.ts
The test setup includes a mock for window.matchMedia in src/test/setup.ts. If you see errors, verify the mock is properly configured.
- Follow the existing code style (enforced by ESLint and Prettier)
- Write tests for new features
- Ensure all tests pass:
npm run test:run - Ensure linting passes:
npm run lint - Ensure formatting is correct:
npm run format:check