# Directory Structure ``` ├── .github │ ├── mcp.json │ └── workflows │ └── release.yml ├── .gitignore ├── .goreleaser.yaml ├── .vscode │ └── settings.json ├── go.mod ├── go.sum ├── LICENSE ├── main_test.go ├── main.go └── README.md ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` .env.test perplexity-mcp-server dist/ ``` -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- ```yaml # This is an example .goreleaser.yml file with some sensible defaults. # Make sure to check the documentation at https://goreleaser.com # The lines below are called `modelines`. See `:help modeline` # Feel free to remove those if you don't want/need to use them. # yaml-language-server: $schema=https://goreleaser.com/static/schema.json # vim: set ts=2 sw=2 tw=0 fo=cnqoj version: 2 before: hooks: - go mod tidy builds: - env: - CGO_ENABLED=0 goos: - linux - windows - darwin archives: - formats: [tar.gz] # this name template makes the OS and Arch compatible with the results of `uname`. name_template: >- {{ .ProjectName }}_ {{- title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }} # use zip for windows archives format_overrides: - goos: windows formats: [zip] release: footer: >- --- Copyright 2025 Alcova AI Pty Ltd. All rights reserved. Released under the [MIT License](https://opensource.org/licenses/MIT). brews: - name: perplexity-mcp repository: owner: Alcova-AI name: homebrew-tap token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" download_strategy: CurlDownloadStrategy homepage: "https://github.com/Alcova-AI/perplexity-mcp" description: "A Model Context Protocol (MCP) server for the Perplexity API" license: "MIT" test: | system "#{bin}/perplexity-mcp --help" install: | bin.install "perplexity-mcp" commit_author: name: "Alcova AI" directory: "Formula" checksum: name_template: 'checksums.txt' snapshot: name_template: "{{ incpatch .Version }}-next" changelog: sort: asc use: github ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Perplexity MCP Server A Model Context Protocol (MCP) server for the Perplexity API written in Go. This server enables AI assistants like Claude (Code and Desktop) and Cursor to seamlessly access Perplexity's powerful search and reasoning capabilities directly from their interfaces. ## Description The Perplexity MCP Server acts as a bridge between AI assistants and the Perplexity API, allowing them to: 1. **Search the web and retrieve up-to-date information** using Perplexity's Sonar Pro model via the `perplexity_ask` tool 2. **Perform complex reasoning tasks** using Perplexity's Sonar Reasoning Pro model via the `perplexity_reason` tool This integration lets AI assistants like Claude access real-time information and specialized reasoning capabilities without leaving their interface, creating a seamless experience for users. ### Key Benefits - **Access to real-time information**: Get current data, news, and information from the web - **Enhanced reasoning capabilities**: Leverage specialized models for complex problem-solving tasks - **Seamless integration**: Works natively with Claude Code, Claude Desktop, and Cursor - **Simple installation**: Quick setup with Homebrew, Go, or pre-built binaries - **Customizable**: Configure which Perplexity models to use for different tasks ## Installation ### Using Homebrew (macOS and Linux) ```sh brew tap alcova-ai/tap brew install perplexity-mcp ``` ### From Source Clone the repository and build manually: ```sh git clone https://github.com/Alcova-AI/perplexity-mcp.git cd perplexity-mcp go build -o perplexity-mcp-server . ``` ### From Binary Releases (Other platforms) Download pre-built binaries from the [releases page](https://github.com/Alcova-AI/perplexity-mcp/releases). ## Usage This server supports only the `stdio` protocol for MCP communication. ### Setup with Claude Code Adding to Claude Code: ```sh claude mcp add-json --scope user perplexity-mcp '{"type":"stdio","command":"perplexity-mcp","env":{"PERPLEXITY_API_KEY":"pplx-YOUR-API-KEY-HERE"}}' ``` That's it! You can now use Perplexity in Claude Code. ### Setup with Claude Desktop Adding to Claude Desktop: 1. Exit the Claude Desktop MCP config: ```sh code ~/Library/Application\ Support/Claude/claude_desktop_config.json ``` 2. Add the Perplexity MCP server: ```diff { "mcpServers": { + "perplexity-mcp": { + "command": "perplexity-mcp", + "args": [ + "--model", + "sonar-pro", + "--reasoning-model", + "sonar-reasoning-pro" + ], + "env": { + "PERPLEXITY_API_KEY": "pplx-YOUR-API-KEY-HERE" + } + } } } ``` ### Command Line Options - `--model, -m`: Specify the Perplexity model to use for search (default: "sonar-pro") - Can also be set with the `PERPLEXITY_MODEL` environment variable - `--reasoning-model, -r`: Specify the Perplexity model to use for reasoning (default: "sonar-reasoning-pro") - Can also be set with the `PERPLEXITY_REASONING_MODEL` environment variable Example: ```sh perplexity-mcp --model sonar-pro --reasoning-model sonar-reasoning-pro ``` ### Direct Execution If you want to run the server directly (not recommended for most users): 1. Set your Perplexity API key as an environment variable: ```sh export PERPLEXITY_API_KEY=your-api-key-here ``` 2. Run the server: ```sh perplexity-mcp ``` ## License MIT ``` -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- ```json { "go.testEnvFile": ".env.test" } ``` -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- ```yaml name: Release on: push: tags: - 'v*' permissions: contents: write packages: write id-token: write attestations: write jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v4 with: go-version-file: go.mod - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser version: latest args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} - name: Attest build provenance uses: actions/attest-build-provenance@v2 with: subject-checksums: ./dist/checksums.txt ``` -------------------------------------------------------------------------------- /.github/mcp.json: -------------------------------------------------------------------------------- ```json { "name": "perplexity-mcp", "version": "0.2.0", "description": "A Model Context Protocol server for the Perplexity API, allowing access to Perplexity's Sonar models for search and reasoning through MCP.", "author": "Ivan Vanderbyl", "license": "MIT", "categories": ["ai", "llm", "api"], "keywords": ["perplexity", "sonar", "chat", "completion", "reasoning", "mcp"], "executable": { "path": "perplexity-mcp" }, "tools": [ { "name": "perplexity_ask", "description": "Engages in a conversation using the Sonar API for search. Accepts an array of messages (each with a role and content) and returns a chat completion response from the Perplexity model." }, { "name": "perplexity_reason", "description": "Uses the Perplexity reasoning model to perform complex reasoning tasks. Accepts a query string and returns a comprehensive reasoned response." } ], "env": [ { "name": "PERPLEXITY_API_KEY", "description": "API key for the Perplexity API", "required": true }, { "name": "PERPLEXITY_MODEL", "description": "Model identifier for search queries (default: sonar-pro)", "required": false }, { "name": "PERPLEXITY_REASONING_MODEL", "description": "Model identifier for reasoning tasks (default: sonar-reasoning-pro)", "required": false } ] } ``` -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- ```go package main import ( "os" "testing" ) func TestPerformChatCompletion(t *testing.T) { // Skip this test if no API key is provided apiKey := os.Getenv("PERPLEXITY_API_KEY") if apiKey == "" { t.Skip("Skipping test: PERPLEXITY_API_KEY environment variable not set") } // Test message messages := []Message{ {Role: "system", Content: "You are a helpful assistant."}, {Role: "user", Content: "What is the capital of France?"}, } // Test with default model result, err := performChatCompletion(apiKey, "sonar-pro", messages) if err != nil { t.Fatalf("Expected no error, got %v", err) } if result == "" { t.Fatalf("Expected non-empty result, got empty string") } // Additional checks can be added here based on expected response format t.Logf("API Response: %s", result) } func TestPerformReasoning(t *testing.T) { // Skip this test if no API key is provided apiKey := os.Getenv("PERPLEXITY_API_KEY") if apiKey == "" { t.Skip("Skipping test: PERPLEXITY_API_KEY environment variable not set") } // Test message for reasoning task messages := []Message{ {Role: "system", Content: "You are a reasoning assistant focused on solving complex problems through step-by-step reasoning."}, {Role: "user", Content: "If a train travels at 120 km/h and another train travels at 80 km/h in the opposite direction, how long will it take for them to be 500 km apart if they start at the same location?"}, } // Test with reasoning model result, err := performChatCompletion(apiKey, "sonar-reasoning-pro", messages) if err != nil { t.Fatalf("Expected no error, got %v", err) } if result == "" { t.Fatalf("Expected non-empty result, got empty string") } // Additional checks can be added here based on expected response format t.Logf("Reasoning API Response: %s", result) } ``` -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- ```go package main import ( "bytes" "context" "encoding/json" "fmt" "io" "log/slog" "net/http" "os" "runtime/debug" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/urfave/cli/v2" ) const ( apiURL = "https://api.perplexity.ai/chat/completions" ) // PerplexityConfig holds configuration for the Perplexity API type PerplexityConfig struct { APIKey string Model string ReasoningModel string } // Message represents a message in the chat completion request type Message struct { Role string `json:"role"` Content string `json:"content"` } // ChatCompletionRequest represents the request to the Perplexity API type ChatCompletionRequest struct { Model string `json:"model"` Messages []Message `json:"messages"` } // ChatCompletionResponse represents the response from the Perplexity API type ChatCompletionResponse struct { ID string `json:"id"` Object string `json:"object"` Created int `json:"created"` Model string `json:"model"` Choices []struct { Message struct { Role string `json:"role"` Content string `json:"content"` } `json:"message"` } `json:"choices"` Citations []string `json:"citations,omitempty"` } // performChatCompletion sends a request to the Perplexity API and returns the response func performChatCompletion(apiKey string, model string, messages []Message) (string, error) { request := ChatCompletionRequest{ Model: model, Messages: messages, } requestBody, err := json.Marshal(request) if err != nil { return "", fmt.Errorf("error marshaling request: %v", err) } req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(requestBody)) if err != nil { return "", fmt.Errorf("error creating request: %v", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+apiKey) client := http.DefaultClient resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("error sending request: %v", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("error reading response body: %v", err) } if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, string(body)) } var response ChatCompletionResponse err = json.Unmarshal(body, &response) if err != nil { return "", fmt.Errorf("error unmarshaling response: %v", err) } if len(response.Choices) == 0 { return "", fmt.Errorf("no choices returned in response") } // Get the message content from the response messageContent := response.Choices[0].Message.Content // Append citations to the message content if they exist if len(response.Citations) > 0 { messageContent += "\n\nCitations:\n" for i, citation := range response.Citations { messageContent += fmt.Sprintf("[%d] %s\n", i+1, citation) } } return messageContent, nil } // parseMessagesFromRequest extracts and validates messages from an MCP tool request func parseMessagesFromRequest(request mcp.CallToolRequest) ([]Message, error) { messagesRaw, ok := request.Params.Arguments["messages"].([]any) if !ok { return nil, fmt.Errorf("'messages' must be an array") } var messages []Message for _, msgRaw := range messagesRaw { msgMap, ok := msgRaw.(map[string]any) if !ok { return nil, fmt.Errorf("invalid message format") } role, ok := msgMap["role"].(string) if !ok { return nil, fmt.Errorf("message must have a 'role' field of type string") } content, ok := msgMap["content"].(string) if !ok { return nil, fmt.Errorf("message must have a 'content' field of type string") } messages = append(messages, Message{Role: role, Content: content}) } return messages, nil } // handlePerplexityAsk handles the perplexity_ask tool request func handlePerplexityAsk(config PerplexityConfig) server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { messages, err := parseMessagesFromRequest(request) if err != nil { return mcp.NewToolResultError(err.Error()), nil } result, err := performChatCompletion(config.APIKey, config.Model, messages) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Error calling Perplexity API: %v", err)), nil } return mcp.NewToolResultText(result), nil } } // handlePerplexityReason handles the perplexity_reason tool request func handlePerplexityReason(config PerplexityConfig) server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { query, ok := request.Params.Arguments["query"].(string) if !ok { return mcp.NewToolResultError("'query' must be a string"), nil } messages := []Message{ {Role: "system", Content: "You are a reasoning assistant focused on solving complex problems through step-by-step reasoning."}, {Role: "user", Content: query}, } result, err := performChatCompletion(config.APIKey, config.ReasoningModel, messages) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Error calling Perplexity API: %v", err)), nil } return mcp.NewToolResultText(result), nil } } // registerPerplexityAskTool creates and registers the perplexity_ask tool func registerPerplexityAskTool(s *server.MCPServer, config PerplexityConfig) { perplexityTool := mcp.NewTool("perplexity_ask", mcp.WithDescription("Engages in a conversation using the Perplexity to search the internet and answer questions. Accepts an array of messages (each with a role and content) and returns a chat completion response from the Perplexity model."), mcp.WithArray("messages", mcp.Required(), mcp.Description("Array of conversation messages"), mcp.Items(map[string]any{ "type": "object", "properties": map[string]any{ "role": map[string]any{ "type": "string", "description": "Role of the message (e.g., system, user, assistant)", }, "content": map[string]any{ "type": "string", "description": "The content of the message", }, }, "required": []string{"role", "content"}, }), ), ) s.AddTool(perplexityTool, handlePerplexityAsk(config)) } // registerPerplexityReasonTool creates and registers the perplexity_reason tool func registerPerplexityReasonTool(s *server.MCPServer, config PerplexityConfig) { reasoningTool := mcp.NewTool("perplexity_reason", mcp.WithDescription("Uses the Perplexity reasoning model to perform complex reasoning tasks. Accepts a query string and returns a comprehensive reasoned response."), mcp.WithString("query", mcp.Required(), mcp.Description("The query or problem to reason about"), ), ) s.AddTool(reasoningTool, handlePerplexityReason(config)) } func main() { app := &cli.App{ Name: "perplexity-mcp", Usage: "A Model Context Protocol server for Perplexity API", Flags: []cli.Flag{ &cli.StringFlag{ Name: "model", Aliases: []string{"m"}, Value: "sonar-pro", Usage: "The model to use for chat completions", EnvVars: []string{"PERPLEXITY_MODEL"}, }, &cli.StringFlag{ Name: "reasoning-model", Aliases: []string{"r"}, Value: "sonar-reasoning-pro", Usage: "The model to use for reasoning tasks", EnvVars: []string{"PERPLEXITY_REASONING_MODEL"}, }, &cli.StringFlag{ Name: "api-key", Aliases: []string{"k"}, Usage: "The API key to use for Perplexity API requests", EnvVars: []string{"PERPLEXITY_API_KEY"}, Required: true, }, }, Action: func(c *cli.Context) error { // Create configuration from CLI arguments config := PerplexityConfig{ APIKey: c.String("api-key"), Model: c.String("model"), ReasoningModel: c.String("reasoning-model"), } buildInfo, ok := debug.ReadBuildInfo() version := "v0.0.1" if ok { version = buildInfo.Main.Version } // Create a new MCP server s := server.NewMCPServer( "perplexity-mcp", version, ) // Register tools registerPerplexityAskTool(s, config) registerPerplexityReasonTool(s, config) // Start the server if err := server.ServeStdio(s); err != nil { return cli.Exit(fmt.Sprintf("Server error: %v", err), 1) } return nil }, } err := app.Run(os.Args) if err != nil { slog.Error("Server error", "error", err) os.Exit(1) } } ```