feat: add MCP server for Perplexica integration#1044
feat: add MCP server for Perplexica integration#1044willtwilson wants to merge 2 commits intoItzCrazyKns:masterfrom
Conversation
Add integrations/mcp-server/ — an MCP SSE server that exposes Perplexica web search as a tool for Claude Desktop, LibreChat, and other MCP-compatible clients. Features: - perplexica_search tool with configurable sources - Uses documented /api/search endpoint (v1.12.1+ API) - Source citations appended as markdown links - Docker support with multi-stage build and non-root user - Client configuration examples for LibreChat and Claude Desktop Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
3 issues found across 6 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="integrations/mcp-server/Dockerfile">
<violation number="1" location="integrations/mcp-server/Dockerfile:11">
P2: Final image includes devDependencies because node_modules is copied from builder without pruning. Run `npm ci --omit=dev` in the final stage to install only production dependencies.</violation>
</file>
<file name="integrations/mcp-server/src/server.ts">
<violation number="1" location="integrations/mcp-server/src/server.ts:109">
P2: Async Express route handler lacks error handling. In Express 4, rejected promises from async handlers are not automatically caught by error middleware and will cause unhandled promise rejections.</violation>
<violation number="2" location="integrations/mcp-server/src/server.ts:118">
P2: Async Express route handler lacks error handling. In Express 4, rejected promises from async handlers are not automatically caught by error middleware and will cause unhandled promise rejections.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
Pull request overview
This PR adds an MCP (Model Context Protocol) server integration that exposes Perplexica's web search as a tool (perplexica_search) for AI agents via SSE transport. It enables usage from MCP-compatible clients like Claude Desktop, LibreChat, Continue, and Cursor. The server forwards queries to Perplexica's existing POST /api/search endpoint and returns synthesized answers with source citations.
Changes:
- New Express-based MCP SSE server (
server.ts) that wraps Perplexica's search API as an MCP tool, with a health endpoint and session management - Multi-stage Docker build with non-root user, plus environment variable configuration for provider/model settings
- Documentation (README) covering setup, client configuration, and environment variables
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
integrations/mcp-server/src/server.ts |
Core MCP server implementation: SSE transport, perplexica_search tool definition, session management, and health check endpoint |
integrations/mcp-server/package.json |
Package definition with MCP SDK, Express, and Zod dependencies |
integrations/mcp-server/Dockerfile |
Multi-stage Docker build with non-root user for production security |
integrations/mcp-server/README.md |
Setup guide covering Docker, manual install, client configuration (LibreChat, Claude Desktop), and environment variable reference |
integrations/mcp-server/.env.example |
Template for required environment variables (provider UUIDs, model keys) |
integrations/mcp-server/tsconfig.json |
TypeScript configuration targeting ES2022 with NodeNext module resolution |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| const server = buildMcpServer(); | ||
| await server.connect(transport); | ||
| res.on("close", () => transports.delete(id)); |
There was a problem hiding this comment.
If server.connect(transport) throws, the res.on("close") cleanup handler is never registered, but the transport has already been added to the transports map (line 112). This creates a potential memory leak where orphaned entries accumulate. Move the res.on("close", ...) handler before await server.connect(transport), or wrap the connect call in a try/catch that cleans up the transport on failure.
| const server = buildMcpServer(); | |
| await server.connect(transport); | |
| res.on("close", () => transports.delete(id)); | |
| res.on("close", () => transports.delete(id)); | |
| const server = buildMcpServer(); | |
| await server.connect(transport); |
There was a problem hiding this comment.
Fixed in follow-up commit (216a67a) — moved res.on('close', ...)) cleanup registration to before server.connect(). If connect throws, the transport is now cleaned from the Map correctly. Good catch on the ordering.
integrations/mcp-server/Dockerfile
Outdated
| COPY package.json tsconfig.json ./ | ||
| RUN npm install |
There was a problem hiding this comment.
There is no package-lock.json included in the PR, and the Dockerfile uses npm install without a lockfile. This means Docker builds are non-reproducible — different builds may resolve different dependency versions. The main Perplexica project uses yarn install --frozen-lockfile for reproducible builds (see Dockerfile.slim:8). Consider committing a package-lock.json (or yarn.lock) and using npm ci (or yarn install --frozen-lockfile) in the Dockerfile for deterministic builds.
| COPY package.json tsconfig.json ./ | |
| RUN npm install | |
| COPY package.json package-lock.json tsconfig.json ./ | |
| RUN npm ci |
There was a problem hiding this comment.
Fixed in follow-up commit (216a67a) — committed a package-lock.json and updated the Dockerfile to use npm ci in the builder stage for deterministic, reproducible builds. You're right that following the same pattern as the main Perplexica project (frozen lockfile installs) is the right approach.
| sources: z | ||
| .array(z.string()) | ||
| .optional() | ||
| .describe( | ||
| 'Sources to search. Defaults to ["web"]. Options: "web", "academic", "discussions"', | ||
| ), |
There was a problem hiding this comment.
The sources parameter accepts any string[] without validation, but the Perplexica API only accepts 'web' | 'discussions' | 'academic' (defined in src/lib/agents/search/types.ts:7). Invalid values passed through could cause unexpected behavior or confusing errors. Consider constraining the zod schema to use z.enum(["web", "academic", "discussions"]) instead of z.string() to catch invalid values at the MCP tool level and provide clear feedback to the AI agent.
There was a problem hiding this comment.
Fixed in follow-up commit (216a67a) — changed z.string() to z.enum(['web', 'academic', 'discussions']) to match the actual Perplexica API types. Also added .default(['web']) so the tool works out-of-the-box without the AI agent needing to specify sources explicitly.
| const PERPLEXICA_CHAT_MODEL = process.env.PERPLEXICA_CHAT_MODEL ?? ""; | ||
| const PERPLEXICA_EMBED_PROVIDER = process.env.PERPLEXICA_EMBED_PROVIDER ?? ""; | ||
| const PERPLEXICA_EMBED_MODEL = process.env.PERPLEXICA_EMBED_MODEL ?? ""; | ||
|
|
There was a problem hiding this comment.
The required environment variables PERPLEXICA_CHAT_PROVIDER, PERPLEXICA_CHAT_MODEL, PERPLEXICA_EMBED_PROVIDER, and PERPLEXICA_EMBED_MODEL default to empty strings, and the server starts without any validation. Every search request will fail at the Perplexica API with "Invalid provider id" when these are not set. Consider adding startup validation that logs a clear error (or exits) if these required variables are missing, so operators get immediate feedback instead of opaque runtime errors on every request.
| // Validate required Perplexica configuration at startup to avoid opaque runtime errors. | |
| const missingPerplexicaEnv: string[] = []; | |
| if (!PERPLEXICA_CHAT_PROVIDER) missingPerplexicaEnv.push("PERPLEXICA_CHAT_PROVIDER"); | |
| if (!PERPLEXICA_CHAT_MODEL) missingPerplexicaEnv.push("PERPLEXICA_CHAT_MODEL"); | |
| if (!PERPLEXICA_EMBED_PROVIDER) missingPerplexicaEnv.push("PERPLEXICA_EMBED_PROVIDER"); | |
| if (!PERPLEXICA_EMBED_MODEL) missingPerplexicaEnv.push("PERPLEXICA_EMBED_MODEL"); | |
| if (missingPerplexicaEnv.length > 0) { | |
| console.error( | |
| "Perplexica MCP server misconfiguration: missing required environment variable(s):", | |
| missingPerplexicaEnv.join(", "), | |
| ); | |
| console.error( | |
| "Please set these environment variables before starting the server to avoid 'Invalid provider id' errors from Perplexica.", | |
| ); | |
| process.exit(1); | |
| } |
There was a problem hiding this comment.
Great suggestion — added a startup validation block that checks all four required env vars and calls process.exit(1) with a clear message listing which are missing. This turns an opaque runtime failure on every search into an immediate, actionable error at startup. Fixed in follow-up commit (216a67a).
integrations/mcp-server/Dockerfile
Outdated
| COPY --from=builder /app/dist dist/ | ||
| COPY --from=builder /app/node_modules node_modules/ | ||
| COPY package.json ./ |
There was a problem hiding this comment.
The Dockerfile copies the full node_modules (including devDependencies like tsx, typescript, and @types/*) into the production image. Since only runtime dependencies are needed, you should either run a separate npm install --omit=dev for the production stage or prune devDependencies before copying. This would reduce the final image size noticeably.
| COPY --from=builder /app/dist dist/ | |
| COPY --from=builder /app/node_modules node_modules/ | |
| COPY package.json ./ | |
| COPY package.json ./ | |
| RUN npm install --omit=dev | |
| COPY --from=builder /app/dist dist/ |
There was a problem hiding this comment.
Fixed in the same commit (216a67a) — the final stage now runs npm ci --omit=dev independently rather than copying node_modules from the builder. This removes tsx, typescript, @types/* and all other devDependencies from the production image.
… Docker build
- Move res.on('close') before server.connect() to prevent transport leak on connect errors
- Add try/catch to both async Express route handlers (Express 4 doesn't propagate async rejections)
- Add startup validation for required env vars with process.exit(1) on missing vars
- Change sources to z.enum(['web', 'academic', 'discussions']) with .default(['web'])
- Add package-lock.json and update Dockerfile to use npm ci (deterministic builds)
- Final Docker stage uses npm ci --omit=dev for production-only dependencies
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
What
Adds an MCP (Model Context Protocol) server that exposes Perplexica's web search as a tool for AI agents.
Why
MCP is becoming the standard protocol for AI tool integration. With this server, Perplexica can be used as a search tool in:
There's no official MCP server for Perplexica today, despite growing demand (related: #996).
What's included
Technical details
Testing
Tested against Perplexica v1.12.1 with:
Summary by cubic
Adds an MCP SSE server that exposes Perplexica’s search as a tool for clients like Claude Desktop and LibreChat. Provides cited results via
perplexica_searchwith Docker support.New Features
integrations/mcp-server(@perplexica/mcp-server) providing an SSE MCP server.perplexica_searchbacked by PerplexicaPOST /api/search(v1.12.1+), with optionalsources./sse,/messages,/health; avoidsexpress.json()to keep raw SSE streams.npm cifor reproducible builds;.env.exampleand README with LibreChat and Claude Desktop examples./api/providers.Bug Fixes
res.on('close')beforeserver.connect()to avoid transport leaks on connect errors.sourcesvalidation toz.enum(['web', 'academic', 'discussions'])with default['web'].Written for commit 216a67a. Summary will update on new commits.