Skip to main content
Glama
anshace

MCP Workstation

by anshace

πŸ–₯️ MCP Workstation

One MCP endpoint. Every MCP server. Your own multi-user platform.

MCP Workstation is a single MCP server that acts as an aggregator: your AI client connects to it once and gets every tool from every MCP server you've registered β€” plus a dense set of built-in tools β€” behind one connection.

Run it in platform mode (set BETTER_AUTH_SECRET) and it becomes a full product:

  • Sign-in with Google or GitHub (Better Auth) at a built-in dashboard (/)

  • Per-user MCP servers β€” each user registers their own stdio/HTTP MCP servers, with secrets stored encrypted (AES-256-GCM), and toggles them on/off

  • API tokens per user; every /mcp request must carry Authorization: Bearer <token>

  • Per-user catalogs β€” tool lists are built per request from that user's enabled servers and module prefs. Users only ever see their own servers.

  • Categorized modules & tools β€” everything ships on by default, grouped by category (Development, Data, Finance & Crypto, …), with per-tool toggles: turn off a whole module or a single tool (e.g. keep crypto_price but hide crypto_trending).

  • A Skills Hub β€” reusable agent instruction sets (debugging, code review, security audits, …) shipped alongside the MCP tools, browsable in the dashboard and loadable by any connected client via skills_list / skills_get. One hub, MCP + skills together.

It speaks the 2026-07-28 MCP specification (the newest release): a stateless protocol core. There is no initialize handshake, no Mcp-Session-Id, no connection state β€” every request is self-contained and independently authenticated, so the server scales behind a plain round-robin load balancer. Tools are namespaced (github_create_issue, mybox_read_file, …) so nothing collides and routing is automatic.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   Bearer token + stateless request   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Claude /    β”‚ ───────────────────────────────────▢ β”‚         MCP Workstation        β”‚
β”‚  Cursor /    β”‚  http://localhost:3125/mcp           β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  VS Code     β”‚ ◀─────────────────────────────────── β”‚  β”‚ Built-in modules (shared) β”‚  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                     β”‚  β”‚  time uuid memory github… β”‚  β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                     β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚  Browser     β”‚  /  (dashboard)                     β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  Google /    β”‚ ──────────────────────────────────▢ β”‚  β”‚ Per-user proxy engine    β”‚  β”‚
β”‚  GitHub      β”‚  /api/auth/*, /api/*                β”‚  β”‚  β–Ά user's stdio servers   β”‚  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                     β”‚  β”‚  β–Ά user's HTTP servers    β”‚  β”‚
                                                     β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
                                                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Quick start

Requires Node.js β‰₯ 22.5 (for the built-in node:sqlite).

1. Run with platform mode (recommended):

npm install
npm run build
cp .env.example .env        # then set BETTER_AUTH_SECRET + OAuth keys (see below)
npm start

2. Or run single-user, no auth (everything is open):

npm start                   # platform mode stays off without BETTER_AUTH_SECRET

You'll see a startup report listing active modules, connected upstream servers, and whether platform mode is on:

[mcp-workstation]   βœ” time: 2 tools
[mcp-workstation]   βœ– github: GITHUB_TOKEN not set
[mcp-workstation] 22 tools available
[mcp-workstation] platform mode: ON (multi-user auth)
[mcp-workstation] dashboard: http://localhost:3125/

Related MCP server: MCPGate

Platform mode β€” turning it on

Add to .env:

BETTER_AUTH_SECRET=$(openssl rand -base64 32)   # REQUIRED β€” turns platform mode on

# Google OAuth (redirect URI: {BETTER_AUTH_URL}/api/auth/callback/google)
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...

# GitHub OAuth (callback URL: {BETTER_AUTH_URL}/api/auth/callback/github)
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...

Then open http://localhost:3125/ and sign in with Google or GitHub. Email/password sign-in is on by default for development (ALLOW_EMAIL_AUTH=false to disable).

From the dashboard you can:

  • Add MCP servers β€” pick stdio (a local command) or http (a remote endpoint), name it (tools appear as name_*), and set env vars / headers. Secrets are encrypted at rest and never returned by the API.

  • Toggle servers on/off β€” disabled servers stop appearing in your endpoint instantly.

  • Mint API tokens β€” name a token (e.g. "Claude Code"), copy it once, revoke anytime.

  • Toggle modules & individual tools β€” everything is on by default; drill into any module and switch off single tools, or hide whole categories you don't use.

  • Browse the Skills Hub β€” enable/disable skills, read their full instructions, and let your clients pull them over MCP.

Connect your AI client

First create an API token in the dashboard. Then point your client at the endpoint with the token as a Bearer header:

Client

Configuration

Claude Code

claude mcp add --transport http workstation http://localhost:3125/mcp + set the Authorization: Bearer <token> header on the connection

Cursor

Settings β†’ MCP β†’ Add new MCP server β†’ Type: http, URL: http://localhost:3125/mcp, Headers: { "Authorization": "Bearer <token>" }

VS Code / Copilot

.vscode/mcp.json β†’ "type": "http", URL + Authorization header

The endpoint serves the 2026-07-28 stateless protocol, falls back to serving 2025-era streamable-HTTP requests automatically, and still bridges the deprecated legacy HTTP+SSE transport for older clients (in platform mode its message POSTs are authenticated too). Requests without a valid token get a proper 401 + WWW-Authenticate challenge.

Local apps can also use stdio (single-user mode):

npm run stdio

What's new β€” 2026-07-28 features wired in

  • Stateless core β€” no handshake, no sessions. Each request carries its protocol version, client identity, and capabilities in a _meta envelope. The old session-recovery/session-reaping code is gone entirely; the server is just a handler.

  • server/discover β€” clients can probe capabilities up front (optional).

  • Cacheable list results β€” tools/list and server/discover return ttlMs + cacheScope hints (tool catalogs only change on reload).

  • Header-based routing β€” requests carry Mcp-Method / Mcp-Name headers, so gateways, rate limiters, and WAFs can route and meter without parsing JSON bodies (missing headers are rejected with a spec-compliant error).

  • resultType: "complete" results with io.modelcontextprotocol/serverInfo in _meta.

  • Built on SDK v2 (@modelcontextprotocol/server + @modelcontextprotocol/client), web-standards based, with the client auto-negotiating protocol era against upstreams.

MCP Registry integration

  • Discover & import: the dashboard Directory searches the official MCP Registry (registry.modelcontextprotocol.io) live β€” one click adds any remote server to your account as a namespaced upstream (/api/registry is a read-only, session-gated proxy; no keys leave the server side).

  • Publish our hub: registry/server.json is a valid entry (io.github.anshace/mcp-workstation, validated offline against the vendored official schema via npm run validate:registry). To list it publicly: point the remote url at your deployed instance, claim the namespace via GitHub, and submit with the official mcp-publisher CLI.

Built-in tools (no API keys needed for the core set)

In platform mode the key-gated modules (github, jira, search, notion, slack) are per-user: each account can store its own credentials on the dashboard's Credentials page (API: /api/secrets, encrypted at rest, never displayed back) and those shadow the server's process env β€” so one user's gh_* calls never run with another's token.

Module

Tools

Enabled by

time

get_current_time, convert_timezone

always

uuid

uuid_generate

always

fetch

fetch_url (timeout, size cap, domain allowlist)

always

memory

memory_set/get/delete/list/search/clear β€” persistent key-value store

always

filesystem

fs_read/write/list/mkdir/remove/stat/search β€” sandboxed to FILESYSTEM_ROOTS

always

sqlite

sqlite_list_tables/query/execute β€” via node:sqlite

always

knowledge

knowledge_index/search/fts_search/vector_search/index_workspace/status/clear β€” full-text (FTS5/BM25) + semantic vector search, zero config

always

github

27 tools β€” gh_get_user/get_repo/create_repo/list_repos/search_repos, issues (list/get/create/update/comment/search), PRs (list/get/create/merge/review), files (get/write/delete), list_commits/branches/releases/create_release, Actions (trigger_workflow/list_workflow_runs), rate_limit

GITHUB_TOKEN

jira

15 tools β€” jira_search_issues (JQL), get/create/update_issue, list_transitions/transition_issue, add_comment/get_comments, add_worklog, list_projects/get_project, list_boards/list_sprints, list_issue_types/list_assignable_users

JIRA_BASE_URL + JIRA_API_TOKEN (+JIRA_EMAIL)

search

web_search, web_extract (Brave / Tavily / Exa)

any of BRAVE_API_KEY, TAVILY_API_KEY, EXA_API_KEY

crypto

crypto_price, crypto_market, crypto_trending, crypto_search, crypto_convert β€” live prices, market data and conversions (CoinGecko)

always

hn

hn_top/new/ask/show, hn_item, hn_search β€” Hacker News stories, threads and full-text search

always

weather

weather_current, weather_forecast, weather_geocode β€” conditions & forecasts (Open-Meteo)

always

Lite catalog β€” search-first, token-frugal (on by default for new users)

Every agent pays for tools/list in its context window β€” a full workstation catalog is 60+ tools β‰ˆ tens of thousands of tokens. Lite mode lists only five Tier-0 tools and keeps everything else fully reachable behind them:

Tool

Role

hub_search_tools

BM25 search over the whole hidden catalog (name + description + module synonyms)

hub_get_tool

fetch the exact input schema of any catalog tool

hub_call

invoke any catalog tool by name β€” rate limits and audit apply identically

workstation_status / workstation_reload

introspection + reload

Toggle per user on the dashboard (Modules & Tools β†’ Lite catalog) or via PUT /api/prefs {"liteCatalog":false} for clients that want the full static list. Retrieval reuses our tool-index work (src/toolsearch.ts, unit-tested against a 20-probe intent set at β‰₯90% accuracy).

Oversized tool results (default >200KB, MAX_RESULT_BYTES) are spilled to a file in the workspace and replaced by a preview + fs_read pointer, so one chatty upstream never floods the agent's context. | skills | skills_list, skills_get β€” pull your enabled skills' instructions over MCP | always |

GitHub and Jira both support enterprise/self-hosted instances via GITHUB_API_URL and JIRA_BASE_URL. Jira accepts an API token (Basic auth with JIRA_EMAIL) or a PAT. | postgres | pg_list_tables/describe_table/query | DATABASE_URL | | notion | notion_search/get_page/list_block_children/create_page/append_blocks | NOTION_TOKEN | | slack | slack_post_message/list_channels/channel_history/list_users | SLACK_BOT_TOKEN | | workstation | workstation_status, workstation_reload | always |

Copy .env.example to .env, fill in the keys you have, and restart. Missing keys simply disable that module β€” everything else keeps working.

🧠 Knowledge base β€” full-text + vector search (zero config)

The knowledge module gives your AI a searchable memory, out of the box:

  • Full-text search β€” SQLite FTS5 with BM25 ranking (knowledge_fts_search)

  • Semantic vector search β€” local embeddings (all-MiniLM-L6-v2, 384-dim), downloaded once and cached; runs fully offline afterwards (knowledge_vector_search)

  • Hybrid search β€” FTS + vector merged and ranked (knowledge_search)

  • Auto-fallback β€” if the embedding model can't load, a TF-IDF vectorizer takes over, so search always works

  • Workspace indexing β€” knowledge_index_workspace crawls the sandboxed filesystem roots and indexes text/code files in chunks

# add documents, then search
knowledge_index        { "text": "...", "title": "...", "source": "..." }
knowledge_search       { "query": "feline companions that purr" }   # semantic
knowledge_fts_search   { "query": "javascript server" }              # exact/BM25

The embedding model downloads on first vector-search use (one-time, ~23 MB), stored in the local HuggingFace cache. No API keys required.

πŸ’‘ Skills Hub β€” instructions your agents can follow

The workstation ships a library of skills: markdown playbooks for recurring work (debugging, code-review, git-workflow, sql-querying, web-research, documentation, deployment-checklist, security-audit β€” each with a description, category and version). They sit next to the MCP tools in one hub:

  • Everything is on by default β€” no setup.

  • The dashboard has a dedicated Skills page: browse by category, read any skill's full instructions in a preview, and toggle skills on/off per user.

  • Connected clients pull them over MCP: skills_list (names, descriptions, categories) then skills_get { "name": "debugging" } for the full content.

  • Skills live in skills/*.md β€” add one with the same frontmatter (name, description, category, version) and rebuild.

Adding your own MCP servers (the aggregator part)

Copy config/servers.example.json to config/servers.json and list the servers you want aggregated. The workstation connects to each at startup (via the v2 client, which auto-negotiates with both modern and legacy upstream servers) and merges their tools into its own list, prefixed with the server's key.

{
  "servers": [
    {
      "key": "myfiles",                       // β†’ tools appear as myfiles_*
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "C:/path/to/folder"],
      "env": { "SOME_TOKEN": "..." }          // optional extra env for the child process
    },
    {
      "key": "remote",
      "type": "http",                         // remote Streamable HTTP / SSE servers
      "url": "https://your-mcp-server.example.com/mcp",
      "headers": { "Authorization": "Bearer ..." }
    }
  ]
}
  • enabled: false (or a bad entry) skips the server; a server that fails to start is reported in workstation_status instead of crashing the workstation.

  • After editing servers.json, call the workstation_reload tool β€” because every HTTP request builds a fresh tool catalog from the live registry, the new tools appear on the very next tools/list.

Operations

  • workstation_status β€” which modules are active (and why others aren't), which upstream servers are connected, total tool count, protocol version.

  • workstation_reload β€” re-reads servers.json, reconnects upstreams, refreshes the tool list.

  • Endpoint path and port are configurable: MCP_PATH (default /mcp), PORT (default 3125).

Security notes

  • Every /mcp request is authenticated in platform mode. No token, no tools. Tokens are stored as SHA-256 hashes, never plaintext; per-user server secrets (env vars, headers) are encrypted with AES-256-GCM using BETTER_AUTH_SECRET and never returned by the API.

  • Multi-tenant isolation. Each request builds the tool catalog from that user's enabled servers and prefs β€” a user can never see or call another user's servers.

  • Filesystem is sandboxed. fs_* tools refuse paths outside FILESYSTEM_ROOTS (default ./data/workspace).

  • Databases are read-only by default. pg_query and sqlite_query block write statements unless you explicitly set PG_ALLOW_WRITE=true / SQLITE_ALLOW_WRITE=true.

  • fetch_url can be restricted to specific domains with allowed_domains.

  • Every key-gated module is off unless you set the key. Nothing phones home.

  • In single-user mode (no BETTER_AUTH_SECRET) there is no auth β€” bind to localhost or put a reverse proxy in front. Sessions use secure cookies automatically when the public URL is HTTPS.

Development

npm run check                 # typecheck + web typecheck + dead-code gate + unit tests
npm run typecheck         # fast type check (backend)
npm run typecheck:web     # type check (React dashboard)
npm run test:unit         # fast unit tests for core modules (no build needed)
npm test                  # builds + unit tests + the core end-to-end smoke test
npm run test:platform     # platform mode: signup β†’ tokens β†’ per-user /mcp β†’ isolation
npm run test:integrations # GitHub + Jira modules against a local mock API (no real credentials)
npm run dev               # run the backend from source (serves the built dashboard at /)
npm run dev:web           # Vite dev server for the dashboard (proxies /api + /mcp to the backend)
npm run build:web         # rebuild the dashboard into public/ (served by the backend)

See CONTRIBUTING.md (how to add a module) and docs/ARCHITECTURE.md (how the pieces fit together).

Project layout

src/
  index.ts            entry point: boots workstation + platform (auth, DB, dashboard)
  server.ts           per-request McpServer built for the authenticated user,
                      shared registry + per-user upstream aggregators
  http.ts             node:http front-end: /api/auth/*, /api/*, static UI, /mcp
                      (Bearer-gated in platform mode, legacy SSE bridge kept)
  config.ts           .env + config/servers.json loading
  registry.ts         mutable tool registry + module registration
  platform/
    auth.ts           Better Auth instance (Google + GitHub, cookies)
    db.ts             SQLite: auth tables (auto-migrated) + servers/tokens/prefs
    tokens.ts         API-token mint/verify + the /mcp Bearer verifier
    api.ts            dashboard REST API (servers CRUD, tokens, prefs, skills,
                      secrets, registry import proxy)
    oauth.ts          OAuth 2.1 authorization server for /mcp (RFC 9728/7591:
                      DCR, consent, PKCE S256 β†’ mcw_ bearer)
    serverConfig.ts   encrypted server-row codec (shared by core + REST)
  toolsearch.ts       ephemeral BM25 tool index + module synonym table
  descli.ts           tool-description quality linter
  serverConfig β†’ platform/serverConfig.ts  server-row ↔ runtime codec
    skills.ts         loads skills/*.md (frontmatter) into the skills hub
    crypto.ts         AES-256-GCM secret encryption + SHA-256 token hashing
  proxy/
    upstream.ts       v2 client connection to one stdio/HTTP MCP server, namespacing
    aggregator.ts     connect-all / list-all / route-calls across upstreams
  builtins/           time, uuid, fetch, memory, filesystem, knowledge, github,
                      jira, search, postgres, sqlite, notion, slack, crypto, hn,
                      weather (+ the skills module)
skills/               *.md β€” the skills hub library (one markdown file per skill)
public/
  index.html          built React dashboard (emitted by `npm run build:web`)
  assets/             hashed JS/CSS bundles (React + Astryx + Tailwind)
web/                  the dashboard source β€” React 19 + Vite + Tailwind CSS v4
                      (layout utilities only), Meta's Astryx design system
                      (@astryxdesign/core + theme-neutral, forced dark),
                      lucide-react (icons)
  src/
    App.tsx           root: session gate + view router + toast viewport
    main.tsx          Astryx Theme provider (neutral theme, dark mode)
    lib/api.ts        REST client + types
    lib/store.tsx     app state: session, data, routing, toasts
    lib/catalog.ts    MCP Directory catalog + connect-guide client configs
    components/       Shell (AppShell + TopNav + SideNav), ui primitives
    views/            Auth, Dashboard, Directory, Connect, Servers, Tokens,
                      Credentials, Modules, Skills, Settings
registry/
  server.json           official MCP Registry entry (publish-ready)
  server.schema.json    vendored registry schema (offline validation)
scripts/
  smoke.mjs           core end-to-end test (npm test)
  smoke-platform.mjs  platform-mode end-to-end test (npm run test:platform)
  smoke-ghjira.mjs    GitHub + Jira mock-API test
  test-upstream.mjs   tiny stdio MCP server used by the platform test
tests/
  *.test.ts           unit tests for core modules (npm run test:unit) β€”
                      utils/config/ratelimit/audit, run via node --test + tsx
docs/
  ARCHITECTURE.md     how the pieces fit together

Roadmap ideas

  • MRTR (Multi Round-Trip Requests) β€” tools that ask the user to confirm mid-call (e.g. before creating a GitHub issue), via input_required results

  • Per-user keys for built-in modules (currently server-level env keys are shared)

  • Tasks extension for long-running agent work

  • Resource + prompt aggregation from upstreams (currently tools only)

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    A centralized management platform that aggregates multiple Model Context Protocol (MCP) servers into a single unified endpoint for AI agents. It provides a web interface for hot-swappable tool management, proxying of existing servers, and AI-powered generation of custom MCP plugins.
    1
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    MCPGate aggregates multiple MCP servers into a single unified endpoint, enabling centralized tool management with granular filtering, automatic namespacing, and observability. Features a real-time web dashboard and optional PostgreSQL-backed audit trails for monitoring and controlling AI tool access across local and remote deployments.
    5 npm
    Apache 2.0
  • F
    license
    Not graded
    quality
    B
    maintenance
    Aggregates multiple MCP servers and custom Python tools behind a single endpoint, with intelligent context and tool discovery for AI agents.
    -