# 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: -------------------------------------------------------------------------------- ``` 1 | .env.test 2 | perplexity-mcp-server 3 | dist/ 4 | ``` -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | 4 | # The lines below are called `modelines`. See `:help modeline` 5 | # Feel free to remove those if you don't want/need to use them. 6 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 7 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 8 | 9 | version: 2 10 | 11 | before: 12 | hooks: 13 | - go mod tidy 14 | 15 | builds: 16 | - env: 17 | - CGO_ENABLED=0 18 | goos: 19 | - linux 20 | - windows 21 | - darwin 22 | 23 | archives: 24 | - formats: [tar.gz] 25 | # this name template makes the OS and Arch compatible with the results of `uname`. 26 | name_template: >- 27 | {{ .ProjectName }}_ 28 | {{- title .Os }}_ 29 | {{- if eq .Arch "amd64" }}x86_64 30 | {{- else if eq .Arch "386" }}i386 31 | {{- else }}{{ .Arch }}{{ end }} 32 | {{- if .Arm }}v{{ .Arm }}{{ end }} 33 | # use zip for windows archives 34 | format_overrides: 35 | - goos: windows 36 | formats: [zip] 37 | 38 | 39 | release: 40 | footer: >- 41 | 42 | --- 43 | Copyright 2025 Alcova AI Pty Ltd. All rights reserved. 44 | Released under the [MIT License](https://opensource.org/licenses/MIT). 45 | 46 | brews: 47 | - name: perplexity-mcp 48 | repository: 49 | owner: Alcova-AI 50 | name: homebrew-tap 51 | token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" 52 | download_strategy: CurlDownloadStrategy 53 | homepage: "https://github.com/Alcova-AI/perplexity-mcp" 54 | description: "A Model Context Protocol (MCP) server for the Perplexity API" 55 | license: "MIT" 56 | test: | 57 | system "#{bin}/perplexity-mcp --help" 58 | install: | 59 | bin.install "perplexity-mcp" 60 | commit_author: 61 | name: "Alcova AI" 62 | directory: "Formula" 63 | 64 | checksum: 65 | name_template: 'checksums.txt' 66 | 67 | snapshot: 68 | name_template: "{{ incpatch .Version }}-next" 69 | 70 | changelog: 71 | sort: asc 72 | use: github 73 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Perplexity MCP Server 2 | 3 | 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. 4 | 5 | ## Description 6 | 7 | The Perplexity MCP Server acts as a bridge between AI assistants and the Perplexity API, allowing them to: 8 | 9 | 1. **Search the web and retrieve up-to-date information** using Perplexity's Sonar Pro model via the `perplexity_ask` tool 10 | 2. **Perform complex reasoning tasks** using Perplexity's Sonar Reasoning Pro model via the `perplexity_reason` tool 11 | 12 | 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. 13 | 14 | ### Key Benefits 15 | 16 | - **Access to real-time information**: Get current data, news, and information from the web 17 | - **Enhanced reasoning capabilities**: Leverage specialized models for complex problem-solving tasks 18 | - **Seamless integration**: Works natively with Claude Code, Claude Desktop, and Cursor 19 | - **Simple installation**: Quick setup with Homebrew, Go, or pre-built binaries 20 | - **Customizable**: Configure which Perplexity models to use for different tasks 21 | 22 | ## Installation 23 | 24 | ### Using Homebrew (macOS and Linux) 25 | 26 | ```sh 27 | brew tap alcova-ai/tap 28 | brew install perplexity-mcp 29 | ``` 30 | 31 | ### From Source 32 | 33 | Clone the repository and build manually: 34 | 35 | ```sh 36 | git clone https://github.com/Alcova-AI/perplexity-mcp.git 37 | cd perplexity-mcp 38 | go build -o perplexity-mcp-server . 39 | ``` 40 | 41 | ### From Binary Releases (Other platforms) 42 | 43 | Download pre-built binaries from the [releases page](https://github.com/Alcova-AI/perplexity-mcp/releases). 44 | 45 | ## Usage 46 | 47 | This server supports only the `stdio` protocol for MCP communication. 48 | 49 | ### Setup with Claude Code 50 | 51 | Adding to Claude Code: 52 | 53 | ```sh 54 | claude mcp add-json --scope user perplexity-mcp '{"type":"stdio","command":"perplexity-mcp","env":{"PERPLEXITY_API_KEY":"pplx-YOUR-API-KEY-HERE"}}' 55 | ``` 56 | 57 | That's it! You can now use Perplexity in Claude Code. 58 | 59 | ### Setup with Claude Desktop 60 | 61 | Adding to Claude Desktop: 62 | 63 | 1. Exit the Claude Desktop MCP config: 64 | 65 | ```sh 66 | code ~/Library/Application\ Support/Claude/claude_desktop_config.json 67 | ``` 68 | 69 | 2. Add the Perplexity MCP server: 70 | 71 | ```diff 72 | { 73 | "mcpServers": { 74 | + "perplexity-mcp": { 75 | + "command": "perplexity-mcp", 76 | + "args": [ 77 | + "--model", 78 | + "sonar-pro", 79 | + "--reasoning-model", 80 | + "sonar-reasoning-pro" 81 | + ], 82 | + "env": { 83 | + "PERPLEXITY_API_KEY": "pplx-YOUR-API-KEY-HERE" 84 | + } 85 | + } 86 | } 87 | } 88 | ``` 89 | 90 | ### Command Line Options 91 | 92 | - `--model, -m`: Specify the Perplexity model to use for search (default: "sonar-pro") 93 | - Can also be set with the `PERPLEXITY_MODEL` environment variable 94 | - `--reasoning-model, -r`: Specify the Perplexity model to use for reasoning (default: "sonar-reasoning-pro") 95 | - Can also be set with the `PERPLEXITY_REASONING_MODEL` environment variable 96 | 97 | Example: 98 | 99 | ```sh 100 | perplexity-mcp --model sonar-pro --reasoning-model sonar-reasoning-pro 101 | ``` 102 | 103 | ### Direct Execution 104 | 105 | If you want to run the server directly (not recommended for most users): 106 | 107 | 1. Set your Perplexity API key as an environment variable: 108 | 109 | ```sh 110 | export PERPLEXITY_API_KEY=your-api-key-here 111 | ``` 112 | 113 | 2. Run the server: 114 | 115 | ```sh 116 | perplexity-mcp 117 | ``` 118 | 119 | 120 | 121 | ## License 122 | 123 | MIT 124 | ``` -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "go.testEnvFile": ".env.test" 3 | } 4 | ``` -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | packages: write 11 | id-token: write 12 | attestations: write 13 | 14 | jobs: 15 | goreleaser: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Set up Go 24 | uses: actions/setup-go@v4 25 | with: 26 | go-version-file: go.mod 27 | 28 | - name: Run GoReleaser 29 | uses: goreleaser/goreleaser-action@v6 30 | with: 31 | distribution: goreleaser 32 | version: latest 33 | args: release --clean 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} 37 | 38 | - name: Attest build provenance 39 | uses: actions/attest-build-provenance@v2 40 | with: 41 | subject-checksums: ./dist/checksums.txt 42 | ``` -------------------------------------------------------------------------------- /.github/mcp.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "perplexity-mcp", 3 | "version": "0.2.0", 4 | "description": "A Model Context Protocol server for the Perplexity API, allowing access to Perplexity's Sonar models for search and reasoning through MCP.", 5 | "author": "Ivan Vanderbyl", 6 | "license": "MIT", 7 | "categories": ["ai", "llm", "api"], 8 | "keywords": ["perplexity", "sonar", "chat", "completion", "reasoning", "mcp"], 9 | "executable": { 10 | "path": "perplexity-mcp" 11 | }, 12 | "tools": [ 13 | { 14 | "name": "perplexity_ask", 15 | "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." 16 | }, 17 | { 18 | "name": "perplexity_reason", 19 | "description": "Uses the Perplexity reasoning model to perform complex reasoning tasks. Accepts a query string and returns a comprehensive reasoned response." 20 | } 21 | ], 22 | "env": [ 23 | { 24 | "name": "PERPLEXITY_API_KEY", 25 | "description": "API key for the Perplexity API", 26 | "required": true 27 | }, 28 | { 29 | "name": "PERPLEXITY_MODEL", 30 | "description": "Model identifier for search queries (default: sonar-pro)", 31 | "required": false 32 | }, 33 | { 34 | "name": "PERPLEXITY_REASONING_MODEL", 35 | "description": "Model identifier for reasoning tasks (default: sonar-reasoning-pro)", 36 | "required": false 37 | } 38 | ] 39 | } ``` -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- ```go 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestPerformChatCompletion(t *testing.T) { 9 | // Skip this test if no API key is provided 10 | apiKey := os.Getenv("PERPLEXITY_API_KEY") 11 | if apiKey == "" { 12 | t.Skip("Skipping test: PERPLEXITY_API_KEY environment variable not set") 13 | } 14 | 15 | // Test message 16 | messages := []Message{ 17 | {Role: "system", Content: "You are a helpful assistant."}, 18 | {Role: "user", Content: "What is the capital of France?"}, 19 | } 20 | 21 | // Test with default model 22 | result, err := performChatCompletion(apiKey, "sonar-pro", messages) 23 | if err != nil { 24 | t.Fatalf("Expected no error, got %v", err) 25 | } 26 | 27 | if result == "" { 28 | t.Fatalf("Expected non-empty result, got empty string") 29 | } 30 | 31 | // Additional checks can be added here based on expected response format 32 | t.Logf("API Response: %s", result) 33 | } 34 | 35 | func TestPerformReasoning(t *testing.T) { 36 | // Skip this test if no API key is provided 37 | apiKey := os.Getenv("PERPLEXITY_API_KEY") 38 | if apiKey == "" { 39 | t.Skip("Skipping test: PERPLEXITY_API_KEY environment variable not set") 40 | } 41 | 42 | // Test message for reasoning task 43 | messages := []Message{ 44 | {Role: "system", Content: "You are a reasoning assistant focused on solving complex problems through step-by-step reasoning."}, 45 | {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?"}, 46 | } 47 | 48 | // Test with reasoning model 49 | result, err := performChatCompletion(apiKey, "sonar-reasoning-pro", messages) 50 | if err != nil { 51 | t.Fatalf("Expected no error, got %v", err) 52 | } 53 | 54 | if result == "" { 55 | t.Fatalf("Expected non-empty result, got empty string") 56 | } 57 | 58 | // Additional checks can be added here based on expected response format 59 | t.Logf("Reasoning API Response: %s", result) 60 | } 61 | ``` -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- ```go 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "log/slog" 10 | "net/http" 11 | "os" 12 | "runtime/debug" 13 | 14 | "github.com/mark3labs/mcp-go/mcp" 15 | "github.com/mark3labs/mcp-go/server" 16 | "github.com/urfave/cli/v2" 17 | ) 18 | 19 | const ( 20 | apiURL = "https://api.perplexity.ai/chat/completions" 21 | ) 22 | 23 | // PerplexityConfig holds configuration for the Perplexity API 24 | type PerplexityConfig struct { 25 | APIKey string 26 | Model string 27 | ReasoningModel string 28 | } 29 | 30 | // Message represents a message in the chat completion request 31 | type Message struct { 32 | Role string `json:"role"` 33 | Content string `json:"content"` 34 | } 35 | 36 | // ChatCompletionRequest represents the request to the Perplexity API 37 | type ChatCompletionRequest struct { 38 | Model string `json:"model"` 39 | Messages []Message `json:"messages"` 40 | } 41 | 42 | // ChatCompletionResponse represents the response from the Perplexity API 43 | type ChatCompletionResponse struct { 44 | ID string `json:"id"` 45 | Object string `json:"object"` 46 | Created int `json:"created"` 47 | Model string `json:"model"` 48 | Choices []struct { 49 | Message struct { 50 | Role string `json:"role"` 51 | Content string `json:"content"` 52 | } `json:"message"` 53 | } `json:"choices"` 54 | Citations []string `json:"citations,omitempty"` 55 | } 56 | 57 | // performChatCompletion sends a request to the Perplexity API and returns the response 58 | func performChatCompletion(apiKey string, model string, messages []Message) (string, error) { 59 | request := ChatCompletionRequest{ 60 | Model: model, 61 | Messages: messages, 62 | } 63 | 64 | requestBody, err := json.Marshal(request) 65 | if err != nil { 66 | return "", fmt.Errorf("error marshaling request: %v", err) 67 | } 68 | 69 | req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(requestBody)) 70 | if err != nil { 71 | return "", fmt.Errorf("error creating request: %v", err) 72 | } 73 | 74 | req.Header.Set("Content-Type", "application/json") 75 | req.Header.Set("Authorization", "Bearer "+apiKey) 76 | 77 | client := http.DefaultClient 78 | resp, err := client.Do(req) 79 | if err != nil { 80 | return "", fmt.Errorf("error sending request: %v", err) 81 | } 82 | defer resp.Body.Close() 83 | 84 | body, err := io.ReadAll(resp.Body) 85 | if err != nil { 86 | return "", fmt.Errorf("error reading response body: %v", err) 87 | } 88 | 89 | if resp.StatusCode != http.StatusOK { 90 | return "", fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, string(body)) 91 | } 92 | 93 | var response ChatCompletionResponse 94 | err = json.Unmarshal(body, &response) 95 | if err != nil { 96 | return "", fmt.Errorf("error unmarshaling response: %v", err) 97 | } 98 | 99 | if len(response.Choices) == 0 { 100 | return "", fmt.Errorf("no choices returned in response") 101 | } 102 | 103 | // Get the message content from the response 104 | messageContent := response.Choices[0].Message.Content 105 | 106 | // Append citations to the message content if they exist 107 | if len(response.Citations) > 0 { 108 | messageContent += "\n\nCitations:\n" 109 | for i, citation := range response.Citations { 110 | messageContent += fmt.Sprintf("[%d] %s\n", i+1, citation) 111 | } 112 | } 113 | 114 | return messageContent, nil 115 | } 116 | 117 | // parseMessagesFromRequest extracts and validates messages from an MCP tool request 118 | func parseMessagesFromRequest(request mcp.CallToolRequest) ([]Message, error) { 119 | messagesRaw, ok := request.Params.Arguments["messages"].([]any) 120 | if !ok { 121 | return nil, fmt.Errorf("'messages' must be an array") 122 | } 123 | 124 | var messages []Message 125 | for _, msgRaw := range messagesRaw { 126 | msgMap, ok := msgRaw.(map[string]any) 127 | if !ok { 128 | return nil, fmt.Errorf("invalid message format") 129 | } 130 | 131 | role, ok := msgMap["role"].(string) 132 | if !ok { 133 | return nil, fmt.Errorf("message must have a 'role' field of type string") 134 | } 135 | 136 | content, ok := msgMap["content"].(string) 137 | if !ok { 138 | return nil, fmt.Errorf("message must have a 'content' field of type string") 139 | } 140 | 141 | messages = append(messages, Message{Role: role, Content: content}) 142 | } 143 | 144 | return messages, nil 145 | } 146 | 147 | // handlePerplexityAsk handles the perplexity_ask tool request 148 | func handlePerplexityAsk(config PerplexityConfig) server.ToolHandlerFunc { 149 | return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 150 | messages, err := parseMessagesFromRequest(request) 151 | if err != nil { 152 | return mcp.NewToolResultError(err.Error()), nil 153 | } 154 | 155 | result, err := performChatCompletion(config.APIKey, config.Model, messages) 156 | if err != nil { 157 | return mcp.NewToolResultError(fmt.Sprintf("Error calling Perplexity API: %v", err)), nil 158 | } 159 | 160 | return mcp.NewToolResultText(result), nil 161 | } 162 | } 163 | 164 | // handlePerplexityReason handles the perplexity_reason tool request 165 | func handlePerplexityReason(config PerplexityConfig) server.ToolHandlerFunc { 166 | return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 167 | query, ok := request.Params.Arguments["query"].(string) 168 | if !ok { 169 | return mcp.NewToolResultError("'query' must be a string"), nil 170 | } 171 | 172 | messages := []Message{ 173 | {Role: "system", Content: "You are a reasoning assistant focused on solving complex problems through step-by-step reasoning."}, 174 | {Role: "user", Content: query}, 175 | } 176 | 177 | result, err := performChatCompletion(config.APIKey, config.ReasoningModel, messages) 178 | if err != nil { 179 | return mcp.NewToolResultError(fmt.Sprintf("Error calling Perplexity API: %v", err)), nil 180 | } 181 | 182 | return mcp.NewToolResultText(result), nil 183 | } 184 | } 185 | 186 | // registerPerplexityAskTool creates and registers the perplexity_ask tool 187 | func registerPerplexityAskTool(s *server.MCPServer, config PerplexityConfig) { 188 | perplexityTool := mcp.NewTool("perplexity_ask", 189 | 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."), 190 | mcp.WithArray("messages", 191 | mcp.Required(), 192 | mcp.Description("Array of conversation messages"), 193 | mcp.Items(map[string]any{ 194 | "type": "object", 195 | "properties": map[string]any{ 196 | "role": map[string]any{ 197 | "type": "string", 198 | "description": "Role of the message (e.g., system, user, assistant)", 199 | }, 200 | "content": map[string]any{ 201 | "type": "string", 202 | "description": "The content of the message", 203 | }, 204 | }, 205 | "required": []string{"role", "content"}, 206 | }), 207 | ), 208 | ) 209 | 210 | s.AddTool(perplexityTool, handlePerplexityAsk(config)) 211 | } 212 | 213 | // registerPerplexityReasonTool creates and registers the perplexity_reason tool 214 | func registerPerplexityReasonTool(s *server.MCPServer, config PerplexityConfig) { 215 | reasoningTool := mcp.NewTool("perplexity_reason", 216 | mcp.WithDescription("Uses the Perplexity reasoning model to perform complex reasoning tasks. Accepts a query string and returns a comprehensive reasoned response."), 217 | mcp.WithString("query", 218 | mcp.Required(), 219 | mcp.Description("The query or problem to reason about"), 220 | ), 221 | ) 222 | 223 | s.AddTool(reasoningTool, handlePerplexityReason(config)) 224 | } 225 | 226 | func main() { 227 | app := &cli.App{ 228 | Name: "perplexity-mcp", 229 | Usage: "A Model Context Protocol server for Perplexity API", 230 | Flags: []cli.Flag{ 231 | &cli.StringFlag{ 232 | Name: "model", 233 | Aliases: []string{"m"}, 234 | Value: "sonar-pro", 235 | Usage: "The model to use for chat completions", 236 | EnvVars: []string{"PERPLEXITY_MODEL"}, 237 | }, 238 | &cli.StringFlag{ 239 | Name: "reasoning-model", 240 | Aliases: []string{"r"}, 241 | Value: "sonar-reasoning-pro", 242 | Usage: "The model to use for reasoning tasks", 243 | EnvVars: []string{"PERPLEXITY_REASONING_MODEL"}, 244 | }, 245 | &cli.StringFlag{ 246 | Name: "api-key", 247 | Aliases: []string{"k"}, 248 | Usage: "The API key to use for Perplexity API requests", 249 | EnvVars: []string{"PERPLEXITY_API_KEY"}, 250 | Required: true, 251 | }, 252 | }, 253 | Action: func(c *cli.Context) error { 254 | // Create configuration from CLI arguments 255 | config := PerplexityConfig{ 256 | APIKey: c.String("api-key"), 257 | Model: c.String("model"), 258 | ReasoningModel: c.String("reasoning-model"), 259 | } 260 | 261 | buildInfo, ok := debug.ReadBuildInfo() 262 | version := "v0.0.1" 263 | if ok { 264 | version = buildInfo.Main.Version 265 | } 266 | 267 | // Create a new MCP server 268 | s := server.NewMCPServer( 269 | "perplexity-mcp", 270 | version, 271 | ) 272 | 273 | // Register tools 274 | registerPerplexityAskTool(s, config) 275 | registerPerplexityReasonTool(s, config) 276 | 277 | // Start the server 278 | if err := server.ServeStdio(s); err != nil { 279 | return cli.Exit(fmt.Sprintf("Server error: %v", err), 1) 280 | } 281 | 282 | return nil 283 | }, 284 | } 285 | 286 | err := app.Run(os.Args) 287 | if err != nil { 288 | slog.Error("Server error", "error", err) 289 | os.Exit(1) 290 | } 291 | } 292 | ```