Feature/chatgpt claude outreach mcp#471
Conversation
Greptile SummaryThis PR adds remote MCP support and ChatGPT-style outreach workflows. The main changes are:
Confidence Score: 3/5These issues should be fixed before merging.
|
| Filename | Overview |
|---|---|
| linkedin_mcp_server/tools/compat.py | Adds generic LinkedIn search/fetch tools, including the arbitrary post URL navigation and result starvation issues. |
| linkedin_mcp_server/tools/outreach.py | Adds draft-first outreach helpers, with validation and contract issues around message length, cadence, and company lookup. |
| linkedin_mcp_server/mcp_auth.py | Adds a static bearer token auth provider for private HTTP MCP deployments. |
Prompt To Fix All With AI
Fix the following 6 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 6
linkedin_mcp_server/tools/compat.py:213-216
**Restrict fetched post URLs**
The `post` branch accepts any absolute URL because `_absolute_linkedin_url()` returns `http://` and `https://` inputs unchanged. A caller can pass `fetch` an id like `post:https://attacker.example/page`, and this branch will navigate the server's browser there through `extract_page`. Since this tool is exposed as a LinkedIn fetch path, it should reject non-LinkedIn origins before navigating.
### Issue 2 of 6
linkedin_mcp_server/tools/compat.py:99-107
**Avoid result starvation**
`_collect_results()` applies one global limit while processing payloads in people, companies, jobs order. When a broad query returns ten people references, this returns before adding any company or job results, even though those searches already ran. The generic `search` tool can therefore silently return only one result type instead of the advertised cross-type search results.
```suggestion
results.append(
{
"id": result_id,
"title": _title_for_reference(reference),
"url": _absolute_linkedin_url(url),
}
)
```
### Issue 3 of 6
linkedin_mcp_server/tools/outreach.py:183
**Validate message length**
`max_chars` has no lower bound, so callers can pass `0` or a negative value. `_trim_to_chars()` then slices with `max_chars - 1`, which returns most of the text plus an ellipsis instead of respecting the requested limit or rejecting the input. Clients using this field to fit LinkedIn message limits can receive oversized drafts.
```suggestion
max_chars: Annotated[int, Field(ge=1)] = 600,
```
### Issue 4 of 6
linkedin_mcp_server/tools/outreach.py:235-237
**Keep plans aligned**
The schedule is built for every requested cadence day, but only three draft strings exist. For `cadence_days=[1, 3, 7, 14]`, the response contains four schedule entries and only three drafts, so clients pairing each day with a draft will drop or mis-handle the final follow-up.
### Issue 5 of 6
linkedin_mcp_server/tools/outreach.py:226
**Reject invalid cadence**
`cadence_days` is used without item validation, and `cadence_days or [3, 7, 14]` treats an explicit empty list as the default schedule. A caller can also pass values like `[-1, 0]` and receive `Day -1` and `Day 0` as valid follow-ups. This should distinguish `None` from an explicit list and reject non-positive day values.
### Issue 6 of 6
linkedin_mcp_server/tools/outreach.py:114-119
**Resolve company slugs**
The parameter is named `company_name`, but this passes the value directly to `scrape_company()`, which constructs `/company/{company_name}`. A normal display name such as `Acme Inc` or a company whose slug differs from its name will navigate to the wrong URL and return missing or wrong company context in the outreach brief. This input should be documented and validated as a LinkedIn slug, or resolved from a company search before scraping.
Reviews (1): Last reviewed commit: "chore: tighten mcp bundle ignores" | Re-trigger Greptile
| results.append( | ||
| { | ||
| "id": result_id, | ||
| "title": _title_for_reference(reference), | ||
| "url": _absolute_linkedin_url(url), | ||
| } | ||
| ) | ||
| if len(results) >= limit: | ||
| return results |
There was a problem hiding this comment.
_collect_results() applies one global limit while processing payloads in people, companies, jobs order. When a broad query returns ten people references, this returns before adding any company or job results, even though those searches already ran. The generic search tool can therefore silently return only one result type instead of the advertised cross-type search results.
| results.append( | |
| { | |
| "id": result_id, | |
| "title": _title_for_reference(reference), | |
| "url": _absolute_linkedin_url(url), | |
| } | |
| ) | |
| if len(results) >= limit: | |
| return results | |
| results.append( | |
| { | |
| "id": result_id, | |
| "title": _title_for_reference(reference), | |
| "url": _absolute_linkedin_url(url), | |
| } | |
| ) |
Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/tools/compat.py
Line: 99-107
Comment:
**Avoid result starvation**
`_collect_results()` applies one global limit while processing payloads in people, companies, jobs order. When a broad query returns ten people references, this returns before adding any company or job results, even though those searches already ran. The generic `search` tool can therefore silently return only one result type instead of the advertised cross-type search results.
```suggestion
results.append(
{
"id": result_id,
"title": _title_for_reference(reference),
"url": _absolute_linkedin_url(url),
}
)
```
How can I resolve this? If you propose a fix, please make it concise.| product_value_proposition: str, | ||
| call_to_action: str, | ||
| tone: str = "warm", | ||
| max_chars: int = 600, |
There was a problem hiding this comment.
max_chars has no lower bound, so callers can pass 0 or a negative value. _trim_to_chars() then slices with max_chars - 1, which returns most of the text plus an ellipsis instead of respecting the requested limit or rejecting the input. Clients using this field to fit LinkedIn message limits can receive oversized drafts.
| max_chars: int = 600, | |
| max_chars: Annotated[int, Field(ge=1)] = 600, |
Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/tools/outreach.py
Line: 183
Comment:
**Validate message length**
`max_chars` has no lower bound, so callers can pass `0` or a negative value. `_trim_to_chars()` then slices with `max_chars - 1`, which returns most of the text plus an ellipsis instead of respecting the requested limit or rejecting the input. Clients using this field to fit LinkedIn message limits can receive oversized drafts.
```suggestion
max_chars: Annotated[int, Field(ge=1)] = 600,
```
How can I resolve this? If you propose a fix, please make it concise.| return { | ||
| "schedule": [f"Day {day}" for day in days], | ||
| "drafts": drafts[: len(days)], |
There was a problem hiding this comment.
The schedule is built for every requested cadence day, but only three draft strings exist. For cadence_days=[1, 3, 7, 14], the response contains four schedule entries and only three drafts, so clients pairing each day with a draft will drop or mis-handle the final follow-up.
Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/tools/outreach.py
Line: 235-237
Comment:
**Keep plans aligned**
The schedule is built for every requested cadence day, but only three draft strings exist. For `cadence_days=[1, 3, 7, 14]`, the response contains four schedule entries and only three drafts, so clients pairing each day with a draft will drop or mis-handle the final follow-up.
How can I resolve this? If you propose a fix, please make it concise.| cadence_days: list[int] | None = None, | ||
| ) -> dict[str, Any]: | ||
| """Plan draft-only LinkedIn follow-ups.""" | ||
| days = cadence_days or [3, 7, 14] |
There was a problem hiding this comment.
cadence_days is used without item validation, and cadence_days or [3, 7, 14] treats an explicit empty list as the default schedule. A caller can also pass values like [-1, 0] and receive Day -1 and Day 0 as valid follow-ups. This should distinguish None from an explicit list and reject non-positive day values.
Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/tools/outreach.py
Line: 226
Comment:
**Reject invalid cadence**
`cadence_days` is used without item validation, and `cadence_days or [3, 7, 14]` treats an explicit empty list as the default schedule. A caller can also pass values like `[-1, 0]` and receive `Day -1` and `Day 0` as valid follow-ups. This should distinguish `None` from an explicit list and reject non-positive day values.
How can I resolve this? If you propose a fix, please make it concise.| if company_name: | ||
| company = await extractor.scrape_company( | ||
| company_name, | ||
| {"about"}, | ||
| callbacks=cb, | ||
| ) |
There was a problem hiding this comment.
The parameter is named company_name, but this passes the value directly to scrape_company(), which constructs /company/{company_name}. A normal display name such as Acme Inc or a company whose slug differs from its name will navigate to the wrong URL and return missing or wrong company context in the outreach brief. This input should be documented and validated as a LinkedIn slug, or resolved from a company search before scraping.
Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/tools/outreach.py
Line: 114-119
Comment:
**Resolve company slugs**
The parameter is named `company_name`, but this passes the value directly to `scrape_company()`, which constructs `/company/{company_name}`. A normal display name such as `Acme Inc` or a company whose slug differs from its name will navigate to the wrong URL and return missing or wrong company context in the outreach brief. This input should be documented and validated as a LinkedIn slug, or resolved from a company search before scraping.
How can I resolve this? If you propose a fix, please make it concise.
No description provided.