Skip to content

Feature/chatgpt claude outreach mcp#471

Open
mendaxfz wants to merge 4 commits into
stickerdaniel:mainfrom
mendaxfz:feature/chatgpt-claude-outreach-mcp
Open

Feature/chatgpt claude outreach mcp#471
mendaxfz wants to merge 4 commits into
stickerdaniel:mainfrom
mendaxfz:feature/chatgpt-claude-outreach-mcp

Conversation

@mendaxfz
Copy link
Copy Markdown

No description provided.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 28, 2026

Greptile Summary

This PR adds remote MCP support and ChatGPT-style outreach workflows. The main changes are:

  • Optional static bearer auth for streamable HTTP deployments.
  • New generic search and fetch tools for data-only MCP clients.
  • New draft-first outreach tools for lead research, message drafting, follow-up planning, and review.
  • README, Docker Hub docs, manifest, and tests updated for the new tools and auth option.

Confidence Score: 3/5

These issues should be fixed before merging.

  • The new fetch tool can navigate the browser to arbitrary external URLs.

  • The generic search path can omit whole result categories for broad queries.

  • Outreach helpers can return invalid lengths, mismatched follow-up plans, or bad company context.

  • linkedin_mcp_server/tools/compat.py

  • linkedin_mcp_server/tools/outreach.py

Security Review

  • Arbitrary URL navigation: linkedin_mcp_server/tools/compat.py lets fetch navigate post: IDs to non-LinkedIn absolute URLs.

Important Files Changed

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

Comment thread linkedin_mcp_server/tools/compat.py
Comment on lines +99 to +107
results.append(
{
"id": result_id,
"title": _title_for_reference(reference),
"url": _absolute_linkedin_url(url),
}
)
if len(results) >= limit:
return results
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
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,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
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.

Comment on lines +235 to +237
return {
"schedule": [f"Day {day}" for day in days],
"drafts": drafts[: len(days)],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

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]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

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.

Comment on lines +114 to +119
if company_name:
company = await extractor.scrape_company(
company_name,
{"about"},
callbacks=cb,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant