Link Finder MCP
# Link Finder MCP
*π«π· [Version franΓ§aise](./README.fr.md)*
An [MCP](https://modelcontextprotocol.io) server for the [Link Finder API](https://app.link-finder.net) β find backlink opportunities, analyze competitors, discover similar domains with AI embeddings, and manage prospecting projects directly from Claude, ChatGPT, Cursor, or any MCP client.
Secrets are provided only through environment variables, and the server is host-agnostic: run it locally over stdio, or deploy it anywhere (Render or any VM) with a bearer-token-protected HTTP/SSE endpoint.
---
## Features
All Link Finder API v2 endpoints are exposed as tools:
| Tool | Endpoint | Plan | What it does |
| --- | --- | --- | --- |
| `get_account` | `getAccount` | Booster | Plan, remaining credits, available features |
| `list_platforms` | `listPlatforms` | Booster | Supported netlinking platforms |
| `list_locations` | `listLocations` | Booster | Countries/locations for keyword search |
| `keyword_search` | `kwSearch` | Booster | Find opportunities from keywords (SERP analysis) |
| `competitor_analysis` | `competitor` | Booster | A competitor's available referring domains |
| `ai_search` | `aiSearch` | Booster | AI prospecting with relevance scoring |
| `similar_domains` | `similarDomains` | Booster | AI-embedding lookalike domains (the gem finder) |
| `create_project` | `createProject` | Booster | Create a project |
| `list_projects` | `listProjects` | Booster | List projects with counts |
| `project_favorites` | `projectFavorites` | Booster | Favorites in a project with full metrics |
| `add_favorite` | `addFavorite` | Booster | Add / remove a domain from a project |
| `update_note` | `updateNote` | Booster | Annotate a standout favorite |
| `check_domain` | `checkDomain` | API | Check one domain across all platforms |
| `bulk_check` | `bulk` | API | Check up to 50,000 domains at once |
| `get_search_history` | _local_ | β | Read locally saved search history |
Plus a guided **prompt** `backlink_workflow` that runs the step-by-step interview and prospecting flow.
The server also follows the API's best practices: every search result is **saved locally** to a `data/` folder and logged in `data/searchHistory.json` so agents can avoid duplicate, credit-wasting searches.
---
## Requirements
- Python 3.10+
- A Link Finder API key β get it in your account at <https://app.link-finder.net/account/> (Booster plan or higher; `checkDomain` and `bulk` need the API plan)
---
## Installation
```bash
git clone https://github.com/<you>/link-finder-mcp.git
cd link-finder-mcp
python -m venv .venv && source .venv/bin/activate # optional but recommended
pip install -r requirements.txt
```
Copy the example environment file and fill in your key:
```bash
cp .env.example .env
# then edit .env and set LINK_FINDER_API_KEY
```
> **No credentials in code.** The API key is read only from `LINK_FINDER_API_KEY` and is never accepted as a tool argument, so it can't leak through the model context.
---
## Configuration
All configuration is via environment variables:
| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `LINK_FINDER_API_KEY` | yes | β | Your Link Finder API key |
| `MCP_TRANSPORT` | no | `stdio` | `stdio` (local), `http` (Streamable HTTP, recommended for hosting), or `sse` (legacy) |
| `MCP_BEARER_TOKEN` | hosted only | β | Shared secret clients send as `Authorization: Bearer <token>` |
| `PORT` | no | `8000` | Port to bind in hosted mode (Render/Railway/Fly inject this) |
| `HOST` | no | `0.0.0.0` | Bind address in hosted mode |
| `MCP_STATELESS_HTTP` | no | `true` | Streamable HTTP only. Stateless = no per-session server state; robust behind proxies/load balancers |
| `MCP_JSON_RESPONSE` | no | `false` | Streamable HTTP only. `true` returns plain JSON instead of SSE-framed responses (only if your client requires it) |
| `LINK_FINDER_DATA_DIR` | no | `data` | Where results + history are saved (empty = disable) |
| `LINK_FINDER_BASE_URL` | no | `https://app.link-finder.net/api/v2` | Override the API base URL |
| `LINK_FINDER_HTTP_TIMEOUT` | no | `120` | HTTP timeout in seconds |
| `MCP_ALLOWED_HOSTS` | no | _(empty)_ | Comma-separated Host allowlist for DNS-rebinding protection. Empty = disabled (works behind any proxy). Supports a `:*` port wildcard. |
| `MCP_ALLOWED_ORIGINS` | no | _(empty)_ | Comma-separated Origin allowlist (used with the above). |
### Which transport?
- **Local clients (Claude Desktop, Cursor, ...)** β `stdio`.
- **Hosted (Render or any VM)** β `http` (**Streamable HTTP**). This is the recommended, proxy-friendly transport; the endpoint lives at **`/mcp`**.
- `sse` is the older transport (endpoint at `/sse`). It works, but long-lived SSE streams can be buffered or reset by PaaS proxies, which can stall the MCP initialization handshake. Prefer `http` unless your client only speaks SSE.
---
## Connect it to your AI chat β pick your setup
There are two ways to use this server. Choose based on your chat app:
| | **A. Local (on your computer)** | **B. Hosted (online URL)** |
| --- | --- | --- |
| **Best for** | Claude Desktop, Cursor, Cline, and other desktop apps | ChatGPT, Claude (web), or any chat that connects to a remote MCP URL |
| **How it runs** | The chat app launches the server for you | You deploy once (e.g. Render), then paste a URL + token |
| **Transport** | `stdio` | `http` (Streamable HTTP) at `/mcp` |
| **Setup** | [Claude Desktop](#a-use-with-claude-desktop-local) Β· [Cursor](#a-use-with-cursor-local) | [Deploy](#deploy-on-render-or-any-vm) then [ChatGPT](#b-use-with-chatgpt-hosted) Β· [any client](#b-use-with-any-other-ai-chat--mcp-client-hosted) |
> Rule of thumb: **desktop app β A (local)**, **web/cloud chat β B (hosted)**.
---
## Running locally (stdio)
```bash
export PYTHONPATH=src
python -m link_finder_mcp.server
```
Or debug interactively with the MCP Inspector:
```bash
PYTHONPATH=src mcp dev src/link_finder_mcp/server.py
```
---
## A. Use with Claude Desktop (local)
Edit your Claude config:
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
```json
{
"mcpServers": {
"link-finder": {
"command": "python",
"args": ["-m", "link_finder_mcp.server"],
"env": {
"PYTHONPATH": "/absolute/path/to/link-finder-mcp/src",
"MCP_TRANSPORT": "stdio",
"LINK_FINDER_API_KEY": "your_link_finder_api_key_here",
"LINK_FINDER_DATA_DIR": "/absolute/path/to/link-finder-mcp/data"
}
}
}
}
```
Restart Claude Desktop. You'll see the Link Finder tools under the tools (hammer) icon. Try:
> "Check my Link Finder credits, then find French backlink opportunities for the keywords `assurance auto;comparateur assurance` with DR 20+ and 500+ traffic. Save the best ones to a new project called *Assurance Q3*."
Claude will chain `get_account` β `keyword_search` β `create_project` β `add_favorite`, then suggest `similar_domains` on the top matches.
> Tip: in Claude Desktop you can also attach the **`backlink_workflow`** prompt (the "+" / prompts menu) to launch the full guided interview.
---
## B. Use with ChatGPT (hosted)
ChatGPT supports remote MCP servers (Developer mode / custom connectors and the Responses API `tools` of type `mcp`). For that you need the server reachable over HTTPS with a bearer token β see [Deploy on Render](#deploy-on-render-or-any-vm) first.
### Option A β ChatGPT Developer Mode / Connectors (UI)
1. Deploy the server (e.g. on Render) with `MCP_TRANSPORT=http` and a strong `MCP_BEARER_TOKEN`.
2. In ChatGPT: **Settings β Connectors β Advanced β Developer mode**, then **Create** a connector.
3. Set the server URL to your deployment's MCP endpoint, e.g. `https://your-app.onrender.com/mcp`.
4. Add an `Authorization` header: `Bearer <your MCP_BEARER_TOKEN>`.
5. Save, then enable the connector in a chat and ask it to find backlinks.
### Option B β OpenAI Responses API (programmatic)
```python
from openai import OpenAI
client = OpenAI()
resp = client.responses.create(
model="gpt-4.1",
tools=[
{
"type": "mcp",
"server_label": "link-finder",
"server_url": "https://your-app.onrender.com/mcp",
"headers": {"Authorization": "Bearer YOUR_MCP_BEARER_TOKEN"},
"require_approval": "never",
}
],
input="Use Link Finder to find Spanish (language 2724) backlink "
"opportunities for 'hosting wordpress' with TF 15+ and report a table.",
)
print(resp.output_text)
```
---
## A. Use with Cursor (local)
Add to `~/.cursor/mcp.json` (or the project `.cursor/mcp.json`):
```json
{
"mcpServers": {
"link-finder": {
"command": "python",
"args": ["-m", "link_finder_mcp.server"],
"env": {
"PYTHONPATH": "/absolute/path/to/link-finder-mcp/src",
"LINK_FINDER_API_KEY": "your_link_finder_api_key_here"
}
}
}
}
```
---
## B. Use with any other AI chat / MCP client (hosted)
Most other clients (Claude on the web, n8n, custom apps, MCP SDKs, ...) connect to a remote MCP server the same way: a **URL** + a **bearer token**. After you [deploy](#deploy-on-render-or-any-vm):
```json
{
"url": "https://your-app.onrender.com/mcp",
"headers": { "Authorization": "Bearer YOUR_MCP_BEARER_TOKEN" }
}
```
- **URL** β your deployment + `/mcp` (Streamable HTTP). Use `/sse` only if your client speaks the legacy SSE transport.
- **Token** β the exact value you set in `MCP_BEARER_TOKEN`.
That's all any compliant MCP client needs. Once connected, just ask in plain language (e.g. *"find backlink opportunities for my coffee blog in France"*) and the model will call the right tools.
---
## Deploy on Render (or any VM)
The server is host-agnostic. In hosted mode it binds `0.0.0.0:$PORT` and protects the MCP endpoints with a bearer token. The recommended hosted transport is **Streamable HTTP** (`MCP_TRANSPORT=http`), served at **`/mcp`**.
A ready-made [`render.yaml`](./render.yaml) is included:
```yaml
services:
- type: web
name: link-finder-mcp
runtime: python
buildCommand: pip install -r requirements.txt
startCommand: python -m link_finder_mcp.server
envVars:
- key: PYTHONPATH
value: src
- key: MCP_TRANSPORT
value: http
- key: LINK_FINDER_API_KEY
sync: false
- key: MCP_BEARER_TOKEN
sync: false
```
1. Push this repo to GitHub.
2. In Render: **New β Blueprint**, point it at the repo.
3. Set the two secret env vars (`LINK_FINDER_API_KEY`, `MCP_BEARER_TOKEN`) in the dashboard.
4. Deploy. Your MCP endpoint will be `https://<service>.onrender.com/mcp`.
The same works on any VM / PaaS β just set the env vars and run `python -m link_finder_mcp.server`. Set `MCP_TRANSPORT=sse` (endpoint `/sse`) only if your client requires the legacy SSE transport.
Point your client at the `/mcp` endpoint with the bearer token:
```json
{
"url": "https://your-app.onrender.com/mcp",
"headers": { "Authorization": "Bearer YOUR_MCP_BEARER_TOKEN" }
}
```
> **Note on saved data:** on ephemeral hosts (like Render's default disk) the `data/` folder is not persistent. Mount a persistent disk, or set `LINK_FINDER_DATA_DIR` to a mounted path, if you want the search history to survive restarts. Local (stdio) usage persists normally.
### Troubleshooting
- **`Failed to validate request: Received request before initialization was complete`** (repeating, on SSE) β the MCP `initialize` handshake is stalling. With the legacy SSE transport the `initialize` response travels back over the long-lived `GET /sse` stream, and PaaS proxies (Render included) often buffer or reset that stream so it never reaches the client. **Fix:** use `MCP_TRANSPORT=http` (Streamable HTTP, endpoint `/mcp`), which doesn't depend on a persistent stream and runs stateless by default.
- **`SSE error: Non-200 status code (421)` / `Invalid Host header`** β this is DNS-rebinding protection rejecting the proxy's public hostname. The server disables host checking by default (the bearer token already guards it), so a fresh deploy works out of the box. If you set `MCP_ALLOWED_HOSTS`, make sure it includes your public host, e.g. `your-app.onrender.com`.
- **`GET / β 404` / `POST <path> β 405` in the logs** β harmless. Each transport serves on its own path (`/mcp` for Streamable HTTP, `/sse` + `/messages/` for SSE); probes hitting other paths/methods are expected. Point your client at the right path for your transport.
---
## How credits work
- Credits are shared across the web app, browser extension, and API.
- `keyword_search` costs 1 `keywords_search` credit **per keyword**; `competitor_analysis` 1 per request; `ai_search` 1 per request; `similar_domains` 1 per domain (or per project search).
- Credits are only consumed when results are found.
- Always call `get_account` first to check remaining credits and which features your plan unlocks.
## Reading results
Each domain result includes fields you can filter and sort on: `title`, `domain`, `dr` (Ahrefs), `tf`/`cf` (Majestic), `rd`, `traffic`, `ttf0` (topic), `ai_lang`, `gg_news`, and per-platform prices (`-2` = not found, `-1` = price unavailable, `>0` = price in the chosen currency). Each platform also has a `_url` field with the direct purchase link, and `best_price_platform` names the cheapest one.
---
## License
MIT β see [LICENSE](./LICENSE).
TDQS
Scored across 15 tools
Most tools have clearly distinct purposes, though 'bulk_check' and 'check_domain' could be confused as both involve checking domains. Descriptions clarify the difference, so disambiguation is strong but not perfect.
Tool names predominantly follow a verb_noun pattern in snake_case (e.g., 'add_favorite', 'check_domain'). A few names deviate like 'project_favorites' (noun_noun) and 'similar_domains' (adjective_noun), but overall consistency is high.
With 15 tools, the count is well-scoped for a link finder/SEO tool. Each tool serves a distinct function without bloat, covering search, management, and account features.
The tool set covers core workflows: finding opportunities (keyword_search, ai_search, similar_domains, competitor_analysis), checking domains (check_domain, bulk_check), and managing favorites/projects. Minor gaps exist, such as lacking a delete_project tool, but the surface is largely complete for the domain.