MCP Marketplace
The MCP Marketplace lets you install, enable and scope external MCP servers from a curated catalog. MCP servers expose tools that the ReAct agent can invoke during chats, extending capabilities without code.
The MCP Marketplace lets you install, enable and scope external MCP servers from a curated catalog. MCP servers expose tools that the ReAct agent can invoke during chats, extending capabilities without code.
Bidirectional MCP (F-MCP-1): beyond the connect direction (external MCPs connected as skills), the expose direction is built in: the internal MCP server exposes the RAG search to external clients.
The system spans Prisma models (McpCatalogEntry, MCPConnection, ChatMCPPin), routes (marketplace.ts, mcp.ts), the agent skill registry (skills.ts), and services (health-check scheduler, DLP filter, audit logging).
The expose side (built-in)
packages/server/src/agent/mcpServer.ts exposes the RAG via the Model Context Protocol to external clients (Cursor, VS Code, and IDEs): the rag_query tool (workspace document search) and list_workspaces. Transports: the primary stateless Streamable HTTP (POST /api/mcp/mcp, MCP 2026-07-28 core, JSON-only responses) + the legacy SSE (GET /api/mcp/sse + POST /api/mcp/message, deprecated in v2 with a one-year grace). Auth: with MCP_API_KEY set → Authorization: Bearer <MCP_API_KEY> (401 on miss/wrong); unset → the loopback-only fallback (127.0.0.1/::1/^::ffff:127.0.0.1) - remote → 401.
Concepts
- `McpCatalogEntry` - catalog template (27 seeded entries: 12 placeholders + 15 curated). Fields:
name,url,transportType(sse/streamable-http),headers(JSON string),description,category,version,author,verificationTier(official/verified_community/unverified),healthStatus(healthy/stale/down),lastHealthCheck,lastHealthError,lastCommitDate,consecutiveFailures. - `MCPConnection` - workspace-scoped connection.
source(manual/marketplace),catalogEntryId(FK when from the marketplace),enabled,headers(JSON string),workspaceId/projectId. - `ChatMCPPin` - per-chat scoping.
@@unique([chatId, connectionId]). Hard-deleted on unpin.
Catalog (browse + install)
Browsing (list and detail) requires only JWT authentication; write operations require authMiddleware + requireAdmin. The frontend filters search/category client-side.
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /api/mcp-marketplace | JWT | List; ?workspaceId query (adds isInstalled) |
| GET | /api/mcp-marketplace/:entryId | JWT | Entry detail |
| POST | /api/mcp-marketplace | JWT + admin | Create catalog entry |
| POST | /api/mcp-marketplace/:entryId/install | JWT + admin | Install into the workspace |
| POST | /api/mcp-marketplace/:entryId/uninstall | JWT + admin | Uninstall |
Install flow (marketplace.ts:165)
- Validate
installMcpServerSchema({ workspaceId: uuid, name?, headers? }); override headers are validated write-side withmcpHeadersSchema(D-12: hop-by-hop blocklist, name regex, size limits). - Check the entry exists (404), not already installed (409,
source: "marketplace"filter - a manual connection sharing the samecatalogEntryIddoes not block). - Create the
MCPConnection:source="marketplace",catalogEntryId,url/transportTypecopied from the catalog,enabled=true,name = override || catalog.name. logEvent("mcp_connection", ..., "mcp.installed", ...).- Fire-and-forget
connectMCPServer(connection.id)- failure is logged, not propagated (201 returned anyway).
Uninstall flow (marketplace.ts:291)
- Find the
MCPConnectionby{ catalogEntryId, workspaceId, source: "marketplace" }(404 if none). disconnectMCPServer+unregisterSkillsForConnection(removes themcp_<conn>_prefixed skills).- Hard delete the
MCPConnection(no soft delete). logEvent("mcp.uninstalled").
Re-install = redo the install flow (a new MCPConnection with a new ID).
Manual connections (CRUD)
/api/mcp-connections (mcp.ts, authMiddleware + requireAdmin): GET, GET /statuses (live from the activeConnections Map, not the DB), POST create, PUT /:id (auto-disconnect/reconnect when enabled), DELETE /:id, POST /:id/toggle, POST /:id/test (10s timeout, returns { success, toolCount?, error? }).
Marketplace-installed connections are indistinguishable from manual ones at the CRUD level - the source field differentiates the origin.
MCP client (agent/mcpClient.ts)
connectMCPServer(connectionId)(:233): ensureConnected cache hit → read-side header validation (mcpHeadersSchemaD-12), transport fallback D-09 (streamable-httpprimary → 4xx → SSE fallback; declaredsse= SSE only; 5xx does not trigger fallback),Client.listTools()→ registers each tool as a skillmcp_<connectionId>_<toolName>(UUID prefix collision-free).- Per-connection mutex
withConnectionLock. activeConnections: Map<connectionId, MCPConnectionState>(not persisted, rebuilt on restart; the workspace/projectscopeis captured at connect time for the D-14 filter).disconnectMCPServer(:363): delete-first + unregister +client.close().shutdownMCPConnectionsfor graceful shutdown.resolveMcpSourceName(toolName)→ human-readable name for themcpSourcesSSE field.
Per-chat pinning
Pins restrict which MCP connections provide tools to a specific chat.
/api/chats/:chatId/pins (mcpPins.ts): GET, POST (createMcpPinSchema), DELETE /:pinId.
Resolution (agent/skills.ts:136 resolveSkillsForChat):
- No pins → every enabled MCP connection of the workspace provides skills.
- Pins present → filters connections pinned AND enabled AND same workspace (both-null globals included; project-scoped excluded).
- All pinned disabled/out-of-scope → fallback to workspace defaults + warn log.
- MCP-02 union (strict, D-04): builtins + pinned MCP skills + a union with active workspace-scoped tools (pins never remove tools - they only add).
Health check
services/mcpHealthCheckJob.ts - pg-boss cron every 30 min (initMCPHealthCheckScheduler() from src/index.ts; */30 * * * *, no fallback timer when pg-boss is unavailable - D-02). It cycles over the installed + enabled connections carrying a catalogEntryId: pings url with exponential retry (1s/2s/4s, 10s timeout), updates healthStatus (healthy / stale on the 1st-2nd failure / down from the 3rd), lastHealthCheck, lastHealthError, consecutiveFailures. Non-blocking: stale/down entries stay visible with a warning badge; recovery starts from a successful ping.
Trust layer (verification tier)
verificationTier is seed-data-only (not written at runtime):
- official - maintained by the Simmetric Chat project.
- verified_community - approved by the maintainers.
- unverified - community, not reviewed.
lastCommitDate = recency indicator. Tiers are informational (they do not block install) but rendered as badges. Installing unverified/down entries is at the admin's risk.
Audit logging
Every operation logs via eventLogService.logEvent with entityType = "mcp_connection":
| Action | Event action |
|---|---|
| Marketplace install | mcp.installed |
| Marketplace uninstall | mcp.uninstalled |
| Enable connection | mcp.enabled |
| Disable connection | mcp.disabled |
| Delete connection | mcp.deleted |
In Community, events go through logEvent (a no-op write without the Enterprise plugin, webhook dispatch still active); with audit_log_immutable (Enterprise) events are written to the immutable audit log. Filter by entityType = "mcp_connection" for the full audit trail.
mcpSources in SSE
The SSE done event includes mcpSources: string[] - the names of the MCP connections whose tools were invoked. Derived from the tool-call prefix (mcp_<conn>_<tool>) via resolveMcpSourceName. The frontend shows the source chips on the message; the widget proxy relays the event byte-verbatim, so the data stays available client-side.
How to use it (admin)
- Settings → MCP Connections (Advanced sub-section) or /mcp-marketplace (admin/superuser).
- Browse the catalog (search, category filter, tier/health badges - client-side filters).
- Install into a workspace → "Installed" badge + contextual Uninstall menu.
- (Optional) Pin specific tools to a chat via
McpPinnerPopoverin the chat header. - Test the connection (
POST /:id/test) to verify the live toolCount.
How to extend
- New catalog entry:
POST /api/mcp-marketplace(admin) or a direct Prisma seed (seedCatalogEntries, idempotent upsert - runtimeinstallCount-like fields are never overwritten by a re-seed). - New transport: extend
buildTransport(mcpClient.ts:136) +TransportKind. - Custom scope:
getMCPToolsForWorkspace(workspaceId)filters byscope.workspaceId(globals included).
v0.28.1 Provenance
| Statement | Source |
|---|---|
| The expose direction (rag_query/list_workspaces, stateless streamable + legacy SSE transports, MCP_API_KEY/loopback gate) | packages/server/src/agent/mcpServer.ts:1-100 (row-time probe 35-02) + README.md:19 - F-MCP-1 |
| The marketplace = admin role | docs/MCP_MARKETPLACE.md:3 (row-time) |
Operational reference - Simmetric Chat v0.28.1 (docs updated 2026-10-02, census SHA `aca5b8ff`).
Cross-link
- Endpoints: 04 - API Reference
- Agent loop: 05 - Feature Guide
- License/audit flags: 03 - Configuration / 08 - Administration