Back to docs
Guide 07

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.

5 min read

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.

MethodPathAuthNotes
GET/api/mcp-marketplaceJWTList; ?workspaceId query (adds isInstalled)
GET/api/mcp-marketplace/:entryIdJWTEntry detail
POST/api/mcp-marketplaceJWT + adminCreate catalog entry
POST/api/mcp-marketplace/:entryId/installJWT + adminInstall into the workspace
POST/api/mcp-marketplace/:entryId/uninstallJWT + adminUninstall

Install flow (marketplace.ts:165)

  1. Validate installMcpServerSchema ({ workspaceId: uuid, name?, headers? }); override headers are validated write-side with mcpHeadersSchema (D-12: hop-by-hop blocklist, name regex, size limits).
  2. Check the entry exists (404), not already installed (409, source: "marketplace" filter - a manual connection sharing the same catalogEntryId does not block).
  3. Create the MCPConnection: source="marketplace", catalogEntryId, url/transportType copied from the catalog, enabled=true, name = override || catalog.name.
  4. logEvent("mcp_connection", ..., "mcp.installed", ...).
  5. Fire-and-forget connectMCPServer(connection.id) - failure is logged, not propagated (201 returned anyway).

Uninstall flow (marketplace.ts:291)

  1. Find the MCPConnection by { catalogEntryId, workspaceId, source: "marketplace" } (404 if none).
  2. disconnectMCPServer + unregisterSkillsForConnection (removes the mcp_<conn>_ prefixed skills).
  3. Hard delete the MCPConnection (no soft delete).
  4. 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 (mcpHeadersSchema D-12), transport fallback D-09 (streamable-http primary → 4xx → SSE fallback; declared sse = SSE only; 5xx does not trigger fallback), Client.listTools() → registers each tool as a skill mcp_<connectionId>_<toolName> (UUID prefix collision-free).
  • Per-connection mutex withConnectionLock.
  • activeConnections: Map<connectionId, MCPConnectionState> (not persisted, rebuilt on restart; the workspace/project scope is captured at connect time for the D-14 filter).
  • disconnectMCPServer (:363): delete-first + unregister + client.close().
  • shutdownMCPConnections for graceful shutdown.
  • resolveMcpSourceName(toolName) → human-readable name for the mcpSources SSE 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":

ActionEvent action
Marketplace installmcp.installed
Marketplace uninstallmcp.uninstalled
Enable connectionmcp.enabled
Disable connectionmcp.disabled
Delete connectionmcp.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)

  1. Settings → MCP Connections (Advanced sub-section) or /mcp-marketplace (admin/superuser).
  2. Browse the catalog (search, category filter, tier/health badges - client-side filters).
  3. Install into a workspace → "Installed" badge + contextual Uninstall menu.
  4. (Optional) Pin specific tools to a chat via McpPinnerPopover in the chat header.
  5. 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 - runtime installCount-like fields are never overwritten by a re-seed).
  • New transport: extend buildTransport (mcpClient.ts:136) + TransportKind.
  • Custom scope: getMCPToolsForWorkspace(workspaceId) filters by scope.workspaceId (globals included).

v0.28.1 Provenance

StatementSource
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 roledocs/MCP_MARKETPLACE.md:3 (row-time)

Operational reference - Simmetric Chat v0.28.1 (docs updated 2026-10-02, census SHA `aca5b8ff`).