使用说明
MCP Workbench Cloud
A cloud-ready, self-hostable AI workbench. Chat with any model, give it tools through MCP, teach it skills, install Claude Code plugins, ground it in your own documents, and measure what it does.
Features · Screenshots · Quick start · Architecture · Security · Development · Roadmap
Why MCP Workbench Cloud
Most chat UIs stop at "pick a model, type a prompt". MCP Workbench Cloud is the bench around the model:
| You want to… | MCP Workbench Cloud gives you |
|---|---|
| Use whichever model fits the job | 12 chat providers behind one streaming chat (OpenAI, Anthropic, Google, Groq, OpenRouter, Together, Mistral, Cohere, Hugging Face, Ollama, LM Studio, any OpenAI-compatible server) |
| Let the model act, safely | Remote MCP servers with per-server approval policies and signed approvals |
| Reuse expertise | Agents, Agent Skills (SKILL.md) and slash commands |
| Bring the ecosystem in | Claude Code plugins and marketplaces installed straight from GitHub |
| Answer from your own data | Knowledge bases with hybrid (vector + keyword) retrieval and citations |
| Choose and justify models | Side-by-side comparison and usage analytics |
| Go beyond text | Studio for image generation, speech-to-text and text-to-speech |
| Automate and share | Scheduled agents, shareable read-only chats and conversation branching |
It is multi-user from the ground up: every record is scoped to its owner, secrets are encrypted, and uploaded files are private.
Features
💬 Chat
🤖 Agents
🔌 MCP
|
🧩 Skills, commands and plugins
📚 Knowledge (RAG)
📊 Compare, measure, explore
🎨 Studio
|
Screenshots
Compare models |
Usage analytics |
Knowledge bases and retrieval |
Plugins from a marketplace |
Dataset column profile |
Embeddings playground |
Agents (light theme) |
Settings and MCP config import |
Studio: images, speech to text, text to speech |
Scheduled agents |
Shareable read-only chat (light theme) |
Quick start
Requirements: Bun and PostgreSQL with pgvector (the dev compose file runs it for you).
git clone https://github.com/scorpiontaj/mcp-workbench.git
cd mcp-workbench
bun install
cp .env.example .env
# Required: ENCRYPTION_KEY and BETTER_AUTH_SECRET (openssl rand -base64 32), ADMIN_EMAILS (your email)
bun run docker:dev # PostgreSQL + pgvector on localhost:5432
bun run db:migrate # tables, the vector extension and built-in agents
bun run dev # http://localhost:3000
- Sign up. The emails in
ADMIN_EMAILSbecome admins; after that, admins manage roles on /admin/users. - Add a provider on Providers: an API key, or the URL of a local Ollama / LM Studio.
- Start a chat, or create a knowledge base with an embedding model such as
text-embedding-3-smallornomic-embed-text.
Production: bun run docker:prod builds a multi-stage image that runs as non-root; then apply migrations as described in
docker/README.md. The app container loads the repo-root .env, with DATABASE_URL and
UPLOAD_DIR overridden to fit the stack. Provider keys in .env (OPENAI_API_KEY, ...) therefore reach the
container: on a public deployment set SERVER_KEYS=admins or off, and ALLOW_PRIVATE_PROVIDER_URLS=false.
Scaling out: to run more than one app instance (or on serverless hosting), set REDIS_URL so rate limits are
shared and survive restarts, and the S3_* variables so every instance reads and writes the same uploads. Without
them, each instance keeps its own rate-limit counters and its own UPLOAD_DIR. Sessions already live in Postgres.
SaaS launch setup
The repository is ready to run as a multi-user service for individual accounts. Before inviting customers:
- Set production
BETTER_AUTH_URL,BETTER_AUTH_SECRET,ENCRYPTION_KEY,DATABASE_URL, and an email transport. Email verification is enforced when email is configured. - Set
SERVER_KEYS=allonly if customers should use the operator's model keys. Then setSERVER_KEYS_FREE_DAILY_LIMIT_USDand, if Stripe Pro is offered,STRIPE_PRO_DAILY_LIMIT_USD.SERVER_KEYS_DAILY_LIMIT_USDremains the operator's maximum per non-admin account across plans. Production defaults shared keys to admins whenSERVER_KEYSis unset. - To offer Pro, create a recurring Stripe Price and configure
STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET,STRIPE_PRICE_PRO, andSTRIPE_PRO_DAILY_LIMIT_USD. Enable the Stripe customer portal and registerhttps://<your-domain>/api/billing/webhookforcustomer.subscription.created,customer.subscription.updated, andcustomer.subscription.deleted. Billing stays unavailable until the complete configuration is valid. - Fill
LEGAL_ENTITY_NAME,LEGAL_ENTITY_ADDRESS,LEGAL_EFFECTIVE_DATE,SUPPORT_EMAIL,PRIVACY_EMAIL, andLEGAL_TERMS_VERSION. The/termsand/privacypages are starter templates, not reviewed legal advice; complete the jurisdiction and retention details and have counsel review them before launch. - Use private S3-compatible storage and Redis for multiple app instances. Configure, test, and document a backup retention period; deletion removes live records and files, while backup copies expire according to that operator policy.
New sign-ups must accept the Terms and Privacy Policy; the acceptance date and version are recorded. Settings provides account export and deletion. Exports include user files up to 100 MB and omit authentication tokens and provider credentials. Deleting an account cancels its Stripe subscriptions before removing the account and its files.
Environment variables
| Variable | Purpose |
|---|---|
DATABASE_URL |
PostgreSQL with pgvector |
ENCRYPTION_KEY |
Encrypts provider keys and MCP headers (required, 32+ chars) |
BETTER_AUTH_SECRET, BETTER_AUTH_URL |
Session signing and the app's public URL (required) |
ADMIN_EMAILS |
Comma-separated emails promoted to admin on sign-up (later changes happen on /admin/users) |
GITHUB_CLIENT_ID/SECRET, GOOGLE_CLIENT_ID/SECRET |
Optional OAuth sign-in |
ALLOW_PRIVATE_PROVIDER_URLS |
Allow LAN/loopback provider URLs (default: on outside production) |
UPLOAD_DIR |
Where private uploads are stored (default ./storage/uploads) |
S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY |
Store uploads in an S3-compatible bucket (AWS S3, Cloudflare R2, MinIO, Backblaze B2) instead of UPLOAD_DIR. Files are still served only through the authenticated /uploads/... route; keep the bucket private. Files not found in the bucket are read from local disk, so bun scripts/migrate-uploads.ts --to-s3 can copy existing uploads over at any time |
S3_REGION, S3_ENDPOINT, S3_FORCE_PATH_STYLE, S3_PREFIX |
Bucket region (default us-east-1, which R2 and MinIO accept; Backblaze needs its own, e.g. us-west-004), endpoint URL for non-AWS services (e.g. https://<account>.r2.cloudflarestorage.com, http://minio:9000), true for path-style URLs (MinIO), and an optional key prefix such as uploads/ |
GITHUB_TOKEN |
Raises GitHub rate limits for skill and plugin imports |
OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY |
Optional server-wide fallback keys |
REDIS_URL |
Optional cache, and shared rate-limit counters for all instances (sign-in limits included). If Redis becomes unreachable, each instance falls back to its own in-memory limits |
SCHEDULER |
off stops this instance from running scheduled agents (several instances can safely run it together) |
SENTRY_DSN |
Optional Sentry error monitoring; set a project DSN to enable it. Reports omit request data, user data, breadcrumbs, error messages, and local variables |
RESEND_API_KEY or SMTP_URL, EMAIL_FROM |
Email for sign-up verification, password reset and scheduled-run notifications: Resend's API, or any SMTP server (smtps://user:pass@host:465). Without either, emails are skipped with a log line and "Forgot password?" is hidden |
LEGAL_ENTITY_NAME, LEGAL_ENTITY_ADDRESS, LEGAL_EFFECTIVE_DATE, LEGAL_TERMS_VERSION, SUPPORT_EMAIL, PRIVACY_EMAIL |
Operator details, policy effective date and version, and legal/support contacts shown on the Terms and Privacy pages and recorded at sign-up. Complete the templates before accepting customers |
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_PRO, STRIPE_PRO_DAILY_LIMIT_USD |
Optional Pro subscriptions. Requires a recurring Stripe Price, configured customer portal, and the subscription webhook events listed above; incomplete configuration keeps billing disabled |
SERVER_KEYS, SERVER_KEYS_FREE_DAILY_LIMIT_USD, SERVER_KEYS_DAILY_LIMIT_USD |
Shared provider key access and spend caps. Production defaults to admins; production free-tier use defaults to $0/day unless a free allowance or operator cap is configured. The operator cap also applies to paid plans |
REQUIRE_EMAIL_VERIFICATION |
true/false forces email verification before email/password sign-in on or off. Default: on when an email transport is configured, off otherwise. OAuth accounts are verified by the provider |
RATE_LIMIT |
off disables rate limiting. Limits are per user (chat 30/min, compare and Studio 10/min, Compare batch and judge runs 120/min, uploads 20/min), per API key (120/min), per shared link for password attempts (10/min) and per IP for sign-in (5/min), sign-up (3/min) and password-reset or verification emails (3/min). Counted in Redis when REDIS_URL is set, else in memory on each instance |
Architecture
MCP Workbench Cloud is a single Next.js 16 application: React pages for the UI and route handlers for the API, backed by PostgreSQL. Everything model-related goes through the Vercel AI SDK, so every provider gets the same streaming, tools, approvals and embeddings.
System overview
flowchart LR
subgraph Browser["Browser"]
UI["Next.js pages<br/>HeroUI v3 + Tailwind v4"]
Py["Pyodide notebook<br/>(Web Worker)"]
end
subgraph Server["Next.js server"]
Proxy["proxy.ts<br/>auth redirect + CSRF guard"]
API["Route handlers /api/*<br/>withUser() → user-scoped"]
Core["lib/<br/>models · mcp · skills · plugins<br/>knowledge · agents · uploads"]
end
subgraph Data["Storage"]
PG[("PostgreSQL<br/>Drizzle ORM<br/>pgvector + tsvector")]
Files[("storage/uploads<br/>private files")]
end
subgraph Outside["External services"]
LLM["LLM providers<br/>OpenAI · Anthropic · Google<br/>Ollama · LM Studio · …"]
MCP["Remote MCP servers<br/>HTTP / SSE"]
GH["GitHub<br/>skills & plugins"]
Reg["Official MCP Registry"]
end
UI -->|"fetch / SSE stream"| Proxy --> API --> Core
Core --> PG
Core --> Files
Core -->|"AI SDK v7"| LLM
Core -->|"ai-sdk/mcp client"| MCP
Core --> GH
Core --> Reg
One chat turn
The client sends only the new message; the server rebuilds the context, runs the model with every tool the chat and its agent allow, pauses for approvals when a server's policy asks for one, and saves the turn when the stream ends, even if the tab was closed.
sequenceDiagram
autonumber
participant B as Browser (useChat)
participant R as /api/chat
participant DB as PostgreSQL
participant M as Model provider
participant T as Tools (MCP · skills · knowledge)
B->>R: new message (+ model, settings, picked skills and @ mentions)
R->>DB: load chat history, agent, owned files
R->>R: system prompt = agent + chat + skill list + knowledge note
R->>M: streamText(messages, tools)
loop up to the agent's step budget
M-->>R: tool call
alt server policy needs approval
R-->>B: signed approval request
B->>R: approve / deny
end
R->>T: execute
T-->>R: result
R->>M: continue with result
end
M-->>R: text stream
R-->>B: UI message stream (text, tool cards, usage)
R->>DB: save turn and token usage
What the model gets for a turn:
| Source | Contributes |
|---|---|
| Agent | Instructions, default temperature, step budget, MCP servers, skills, knowledge bases |
| Chat | Its own system prompt and the MCP servers / knowledge bases switched on in the sidebar |
| MCP servers | Their tools, namespaced <server>__<tool>, each gated by the server's approval policy |
| Skills | Names and descriptions in the prompt; load_skill / read_skill_file fetch the rest when needed |
| Knowledge bases | A note listing them and the search_knowledge tool |
| Composer picks | For that message only: picked skills' full SKILL.md text, @folder summaries, and @ knowledge bases and MCP servers added to the above. Every id is checked against the user's own records (400 otherwise) and stored on the message, so tool approvals and regenerates keep them |
Knowledge pipeline
flowchart TB
subgraph Ingest["Ingestion (background, after the request returns)"]
S["Upload · web page · pasted text · dataset rows"] --> X["Extract text<br/>unpdf · HTML → text · CSV/Parquet → records"]
X --> C["Chunk<br/>recursive splitter, size + overlap"]
C --> E["embedMany()<br/>the base's embedding model"]
E --> V[("KnowledgeChunk<br/>vector + generated tsvector")]
end
subgraph Search["Retrieval: search_knowledge tool"]
Q["Question"] --> QE["embed() per embedding model"]
Q --> KW["OR keyword query<br/>English stemming"]
QE --> VS["cosine distance <=>"]
KW --> FT["ts_rank_cd over GIN index"]
VS --> F["Reciprocal rank fusion"]
FT --> F
F --> P["Top passages + sources → model cites them"]
end
V -.-> VS
V -.-> FT
Each knowledge base fixes its embedding model, so bases built with different models can be searched together: the question is embedded once per model, and the ranked lists are fused.
Data model
erDiagram
user ||--o{ Chat : owns
user ||--o{ Agent : authors
user ||--o{ McpServer : connects
user ||--o{ Skill : has
user ||--o{ Command : has
user ||--o{ Plugin : installs
user ||--o{ KnowledgeBase : owns
user ||--o{ Dataset : uploads
user ||--o{ ProviderConfig : configures
Chat ||--o{ Message : contains
Message ||--o{ Attachment : has
Agent |o--o{ Chat : "linked to"
Plugin ||--o{ Skill : adds
Plugin ||--o{ Command : adds
Plugin ||--o{ Agent : adds
Plugin ||--o{ McpServer : adds
KnowledgeBase ||--o{ KnowledgeDocument : holds
KnowledgeDocument ||--o{ KnowledgeChunk : "split into"
Message {
jsonb parts "text, files, tool calls"
int tokensIn
int tokensOut
}
KnowledgeChunk {
vector embedding
tsvector tsv "generated"
}
McpServer {
text headers "encrypted"
text approval "never / readonly / always"
}
Deleting a plugin cascades to everything it installed; deleting a knowledge base removes its documents
and chunks. The schema lives in lib/schema.ts and the migrations in drizzle/.
Project layout
app/
(pages) chat, agents, compare, skills, plugins, knowledge, datasets, embeddings,
models, providers, tools (MCP inspector), registry, analytics, config, …
api/ route handlers, one folder per resource; every handler is wrapped in withUser()
uploads/[name] serves a private upload to its owner
components/
ui/ design-system primitives: HeroUI components behind a stable props API
chat/ compare/ analytics/ datasets/ skills/ … feature components
hooks/ SWR data hooks (useAgents, useKnowledgeBases, usePlugins, …)
lib/
models.ts provider → AI SDK language/embedding model, per-user keys
mcp.ts MCP clients, tool namespacing, approval policy
skills.ts progressive-disclosure skill tools
composer.ts turn-context.ts "/" and "@" picker parsing; per-message skills and mentions (ownership checks)
bundles.ts plugins.ts github-source.ts SKILL.md / plugin parsing and GitHub fetching
knowledge.ts rag-text.ts ingestion, chunking, hybrid search
tabular.ts datasets.ts CSV/Parquet parsing and column profiling
uploads.ts upload-access.ts private file storage and access checks
schema.ts db.ts Drizzle schema and client
scripts/ mock-llm.ts, mock-mcp.ts, migrate-uploads.ts
drizzle/ SQL migrations
docker/ Dockerfile and compose files
Design decisions
- One AI layer. Every provider goes through the AI SDK (
getModel/getEmbeddingModel), so chat, tools, comparison and embeddings behave the same everywhere. - The server owns the conversation. Clients send the new message only; history, tools and approvals are rebuilt on the server, so a modified client can't rewrite history or forge an approval.
- Pure parsers, thin routes. Skill/plugin parsing, chunking, CSV and MCP-config parsing, rank fusion and PATCH schemas are pure functions with unit tests; routes validate input with zod and stay small.
- Postgres does the retrieval. pgvector and full-text search run side by side in one database, so there's no separate vector store to run or keep in sync.
- Design system behind stable props.
components/uikeeps one component API while rendering HeroUI v3, so the whole app moved to HeroUI at once and can keep evolving.
Local stdio MCP servers
The hosted app can't start processes on your machine, so local servers (the npx … and uvx … kind) run
through a small bridge you start yourself. It launches the server once per session and exposes it as a
Streamable HTTP endpoint protected by OAuth:
bun scripts/mcp-bridge.ts --port 8808 -- npx -y @modelcontextprotocol/server-filesystem ~/notes
It prints a URL and a pairing code. In the MCP Inspector, add http://127.0.0.1:8808/mcp with
authentication OAuth sign-in and sign in: the bridge registers the workbench, opens its approval page, and
you approve with the pairing code from its terminal. Tokens refresh on their own; restarting the bridge signs
clients out and prints a new code, and after 10 wrong codes it stops approving until restarted. Clients without
OAuth can send the Authorization: Bearer <token> header the bridge also prints.
A self-hosted workbench reaches it directly (set ALLOW_PRIVATE_PROVIDER_URLS=true). For the hosted app, expose
it with a tunnel such as cloudflared tunnel --url http://127.0.0.1:8808, add the tunnel's https URL instead,
and start the bridge with --public-url https://<tunnel-host> so its OAuth metadata names that address.
Security model
| Concern | How it's handled |
|---|---|
| Accounts | Better Auth (email/password, optional GitHub/Google); withUser() on every API route; queries scoped to user.id; admins manage roles on /admin/users but never their own, and the last admin stays |
| Secrets | Provider keys and MCP headers encrypted with AES-256-GCM; never returned to the browser |
| Provider spend | Server-wide keys are only used by accounts SERVER_KEYS allows (requests with a personal API key count as a regular user; scheduled runs use the owner's role); free and Pro allowances are capped by SERVER_KEYS_DAILY_LIMIT_USD when configured |
| Files | Stored outside public/, served only to their owner, never inlined into another user's chat |
| Outbound requests | SSRF guard on provider, MCP, plugin and knowledge URLs (private ranges blocked in production, no redirects) |
| Tool use | Per-server approval policy; approval requests are HMAC-signed and decisions are applied to the stored message |
| Cross-site requests | proxy.ts rejects cross-origin mutations (requests with an API key carry no cookie credential and skip this check); strict CSP and security headers |
| API keys | Personal mwb_ keys are shown once and stored as SHA-256 hashes; they act as the user without admin rights, can't manage keys, and are rate-limited per key |
| Sharing | Shared agents never expose the author's MCP servers, skills or knowledge bases; share links can expire and take a password (scrypt hash, per-link signed httpOnly cookie, attempts rate-limited) |
| Local MCP bridge | OAuth 2.1 with PKCE (S256) and dynamic registration; approving a client needs the pairing code printed in the bridge's terminal, so a public tunnel URL alone grants nothing |
| Code execution | The notebook runs Python in the browser (Pyodide); nothing executes on the server |
Development
bun run dev # Next.js dev server
bun run check # typecheck + lint + unit tests (CI also runs a production build)
bun run db:generate # create a migration after editing lib/schema.ts
bun run db:migrate # apply migrations
Calling the API from scripts: create a personal key under Settings → API keys (it is shown once) and send it as a bearer token. Keys work on every user API route except admin routes and key management:
curl -H "Authorization: Bearer mwb_…" http://localhost:3000/api/chats
Mock services for working without API keys:
| Command | Provides |
|---|---|
bun scripts/mock-llm.ts 4010 |
OpenAI-compatible chat model mock-echo, embedding model mock-embed, and Studio endpoints (images, transcription, speech). Add it as an LM Studio provider at http://127.0.0.1:4010/v1 or a custom provider at http://127.0.0.1:4010. Send /call <tool> <json> to make it call a tool. |
bun scripts/mock-mcp.ts 4020 |
An MCP server at http://127.0.0.1:4020/mcp with read-only and destructive tools, a resource and a prompt |
CI (.github/workflows/ci.yml) installs with a frozen lockfile, then runs typecheck, lint, tests and a production build.
Contributing
- Fork the repository and create a branch.
- Run
bun run checkbefore pushing. - Open a pull request describing what changed and how you tested it.
License
MIT, see LICENSE.
Made by Tajeddine Bourhim