#
tokens: 4884/50000 8/8 files
lines: off (toggle) GitHub
raw markdown copy
# 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)
	}
}

```