Summary
doctor reports linkedin and exa_search as ok based on the current working directory, not on whether the MCP is actually configured. Both checks grep the raw output of mcporter config list for a bare substring, and that output contains the project config path — which embeds $CWD. Any directory whose name contains the substring flips the channel to healthy.
exa_search is the worse of the two: it greps for "exa", which is a substring of the very common example.
Reproduction
With no MCP servers configured at all (mcporter config list → No local servers match the provided filters.):
$ mkdir -p /tmp/example-project && cd /tmp/example-project
$ agent-reach doctor --json | jq -r '.exa_search | "\(.status) \(.active_backend)"'
ok Exa via mcporter # ← FALSE: Exa is not configured
$ mkdir -p /tmp/neutral-dir && cd /tmp/neutral-dir
$ agent-reach doctor --json | jq -r '.exa_search | "\(.status) \(.active_backend)"'
off null # ← correct
Same for linkedin from any *linkedin* directory:
$ cd ~/Desktop/linkedin-pipeline
$ agent-reach doctor --json | jq -r '.linkedin.status'
ok # ← FALSE
$ cd /tmp && agent-reach doctor --json | jq -r '.linkedin.status'
off # ← correct
Root cause
mcporter config list always prints the project config path, even when nothing is configured:
No local servers match the provided filters.
Project config: /tmp/example-project/config/mcporter.json (missing)
System config: /Users/me/.mcporter/mcporter.json (missing)
The checks substring-match that whole blob:
agent_reach/channels/linkedin.py:36 — if "linkedin" in probe.output.lower():
agent_reach/channels/exa_search.py:34 — if "exa" in probe.output.lower():
So Project config: /tmp/example-project/... satisfies "exa" in output.
Impact
doctor is documented as the routing source of truth ("动手前先体检 … 按各平台 active_backend 字段选命令组"). A false ok sends the agent down a command path whose backend was never installed, and the resulting failure looks like a broken MCP rather than an unconfigured one. This cost a real debugging session: doctor said linkedin: ok, but mcporter list reported no servers configured — the channel had simply never been set up, and the project directory happened to be named linkedin-pipeline.
Suggested fix
Parse the server list rather than grepping the whole blob. mcporter config list --json returns structured output; match on a server name/key, and scope the match to the servers section instead of the config-path lines. Failing that, at minimum strip the Project config: / System config: trailer before matching, and use a word-boundary/exact-name comparison rather than a bare substring.
A regression test that runs doctor from a tmp_path named example-linkedin with an empty mcporter config would pin both cases.
Second, unrelated issue: references/career.md LinkedIn instructions are stale
references/career.md (and the same hints in channels/linkedin.py) tell the user:
pip install linkedin-scraper-mcp
mcporter config add linkedin http://localhost:3000/mcp
Both lines are dead:
linkedin-scraper-mcp was renamed. On PyPI it is now a transitional stub whose own summary reads "Renamed to mcp-server-linkedin. This transitional package forwards to it." The live package is mcp-server-linkedin (currently 4.17.0). Upstream (https://github.com/stickerdaniel/linkedin-mcp-server) recommends uvx, not pip.
- The transport is wrong. The server is stdio by default;
http://localhost:3000/mcp assumes an HTTP server nobody started. The port is also wrong — --transport streamable-http defaults to :8000/mcp.
- The doc's example selectors (
linkedin-scraper.get_person_profile) don't match the server name in its own config add line (linkedin), and search_people is listed as in-development upstream.
What actually works today:
mcporter config add linkedin --command uvx --arg 'mcp-server-linkedin@latest' \
--env UV_HTTP_TIMEOUT=300 --scope home
uvx mcp-server-linkedin@latest --import-from-browser chrome # or --login
mcporter call 'linkedin.get_feed(num_posts: 5)'
Note mcporter config add defaults to --scope project, which writes into $CWD/config/mcporter.json — worth pinning --scope home in the docs so the channel resolves from any directory. (That default is also what makes the substring bug above so easy to hit.)
Happy to send a PR for either half if useful.
Found while wiring the LinkedIn channel on v1.5.0, macOS, Python 3.14.
Summary
doctorreportslinkedinandexa_searchasokbased on the current working directory, not on whether the MCP is actually configured. Both checks grep the raw output ofmcporter config listfor a bare substring, and that output contains the project config path — which embeds$CWD. Any directory whose name contains the substring flips the channel to healthy.exa_searchis the worse of the two: it greps for"exa", which is a substring of the very commonexample.Reproduction
With no MCP servers configured at all (
mcporter config list→No local servers match the provided filters.):Same for
linkedinfrom any*linkedin*directory:Root cause
mcporter config listalways prints the project config path, even when nothing is configured:The checks substring-match that whole blob:
agent_reach/channels/linkedin.py:36—if "linkedin" in probe.output.lower():agent_reach/channels/exa_search.py:34—if "exa" in probe.output.lower():So
Project config: /tmp/example-project/...satisfies"exa" in output.Impact
doctoris documented as the routing source of truth ("动手前先体检 … 按各平台active_backend字段选命令组"). A falseoksends the agent down a command path whose backend was never installed, and the resulting failure looks like a broken MCP rather than an unconfigured one. This cost a real debugging session:doctorsaidlinkedin: ok, butmcporter listreported no servers configured — the channel had simply never been set up, and the project directory happened to be namedlinkedin-pipeline.Suggested fix
Parse the server list rather than grepping the whole blob.
mcporter config list --jsonreturns structured output; match on a server name/key, and scope the match to the servers section instead of the config-path lines. Failing that, at minimum strip theProject config:/System config:trailer before matching, and use a word-boundary/exact-name comparison rather than a bare substring.A regression test that runs
doctorfrom atmp_pathnamedexample-linkedinwith an empty mcporter config would pin both cases.Second, unrelated issue:
references/career.mdLinkedIn instructions are stalereferences/career.md(and the same hints inchannels/linkedin.py) tell the user:Both lines are dead:
linkedin-scraper-mcpwas renamed. On PyPI it is now a transitional stub whose own summary reads "Renamed to mcp-server-linkedin. This transitional package forwards to it." The live package ismcp-server-linkedin(currently 4.17.0). Upstream (https://github.com/stickerdaniel/linkedin-mcp-server) recommendsuvx, notpip.http://localhost:3000/mcpassumes an HTTP server nobody started. The port is also wrong —--transport streamable-httpdefaults to:8000/mcp.linkedin-scraper.get_person_profile) don't match the server name in its ownconfig addline (linkedin), andsearch_peopleis listed as in-development upstream.What actually works today:
Note
mcporter config adddefaults to--scope project, which writes into$CWD/config/mcporter.json— worth pinning--scope homein the docs so the channel resolves from any directory. (That default is also what makes the substring bug above so easy to hit.)Happy to send a PR for either half if useful.
Found while wiring the LinkedIn channel on v1.5.0, macOS, Python 3.14.