Skip to content

Correct the MCP skill against what the server actually serves - #9

Merged
leenk7991 merged 5 commits into
mainfrom
fix/mcp-skill-drift
Aug 19, 2026
Merged

Correct the MCP skill against what the server actually serves#9
leenk7991 merged 5 commits into
mainfrom
fix/mcp-skill-drift

Conversation

@leenk7991

@leenk7991 leenk7991 commented Aug 17, 2026

Copy link
Copy Markdown
Member

Why

We tested the skill end to end for the first time — hosted server, Cursor, real token — and it was wrong in several places. Some of it would stop someone connecting at all; the rest was content that earns nothing because the server already provides it.

Things that were wrong

Cursor cannot connect using the direct-HTTP form. Its native url transport connects, then opens a GET SSE stream, which this server refuses; the connection dies with Failed to open SSE stream: Not Acceptable and no tools appear. The skill implied mcp-remote was a convenience. This cost an hour to diagnose.

The tool list had already drifted, a week after merging. tools/list on production returns twelve tools; the skill documented ten. Missing were export_dependencies_csv and get_server_instructions.

get_server_instructions is a trap. Its own description says "Always call first", so an agent reading tools/list will open every session with it. It returns an empty string. That contradiction is exactly the kind of thing a skill can say and a schema cannot.

The filter shape was wrong. Only scan_id, project, repo, page and page_size are top-level; everything else nests inside a filters object.

The token isn't as private as implied. Cursor expands ${env:CORGEA_TOKEN} before spawning the bridge, so it lands in mcp-remote's command line where ps can read it.

Things that were redundant

Asked a test project "are any dependencies actually exploitable?" with the skill absent, and the agent filtered on reachability and ranked sensibly anyway. Reading what the server sends explains why: each tool description carries its filters, valid values and sort keys in full, including every reachability state. So the skill's prioritisation advice, its reachability value list and its list-before-get guidance were all restating the schema — earning nothing, and drifting the moment the server changes.

Those are gone. The tool section is now a map of what exists by area, plus the handful of things the schemas genuinely do not say.

What changed

  • Tool inventory replaced with an area map that defers to tools/list.
  • get_server_instructions documented as safe to skip while it returns empty.
  • Kept only non-schema knowledge: nested filters, empty-results-means-missed-filter, rate limits favouring list_* over looped get_*, and export_dependencies_csv returning a URL rather than rows.
  • Cursor block gains --transport http-only, the 406 symptom, and why the bridge is mandatory. Direct-HTTP section names Cursor as a client that fails.
  • Noted that Cursor spawns its own bridge, so nobody runs mcp-remote by hand.
  • Added the ps exposure caveat under token handling.
  • Frontmatter gains CSV export as a trigger.

Net effect is two lines shorter than main despite adding the transport troubleshooting.

Test plan

  • tools/list against production returns exactly the twelve tools listed
  • get_server_instructions on production returns ""
  • Reproduced the 406 with Cursor's url form, and a working connection with the http-only bridge
  • scripts/validate.mjs passes
  • Re-run the reachability question with the trimmed skill installed and confirm the answer is no worse

Related: Corgea/docs#327 carries the same transport fix into the public docs.

leenk7991 and others added 3 commits August 17, 2026 14:49
Testing the skill end to end turned up four things it gets wrong.

The tool list was already stale a week after merging: the server exposes twelve
tools, this file documented ten. Missing were `export_dependencies_csv` and
`get_server_instructions`, whose own description says to always call it first —
which this file contradicted by telling the agent to open with a `list_*` call.
Rather than restate the inventory and watch it drift again, the section now
names the tools by area and sends the reader to `tools/list` and the schemas
their client already holds. That is the same reasoning that slimmed
`corgea-scan`.

The filter shape was wrong. Only `scan_id`, `project`, `repo`, `page` and
`page_size` are top-level; severity, status, branch and the rest go in a nested
`filters` object, and passing them flat is the easy mistake to make.

The Cursor guidance implied the bridge was optional. It is not: Cursor's native
`url` transport connects and then tries to open a GET SSE stream, which this
server refuses, and the connection dies with `Failed to open SSE stream: Not
Acceptable`. The `--transport http-only` bridge is the only shape that works,
and the direct-HTTP section now says to test before trusting it.

Finally, keeping the token out of the config file does not keep it off the
machine — the client expands it before spawning the bridge, so it lands in
`mcp-remote`'s command line where `ps` can read it. Worth saying, since the
previous wording implied otherwise.

Co-authored-by: Cursor <cursoragent@cursor.com>
The tool describes itself as "Always call first", so an agent reading
`tools/list` will open every session with it. On production it returns an empty
string, making that a wasted round trip on a rate-limited API.

This is worth a line precisely because it contradicts what the schema says: the
tool description is the one thing an agent cannot second-guess on its own. If
the server ever starts returning text there, that text describes the server as
deployed and outranks this file.

Co-authored-by: Cursor <cursoragent@cursor.com>
Testing this against production showed the agent filtering SCA findings by
reachability and ranking them sensibly with the skill absent. That is not
surprising once you read what the server sends: each tool description carries
its filters, valid values and sort keys in full, including every reachability
state. A skill restating them earns nothing and goes stale, which is the same
failure as the tool list this PR already cut.

What is left is what the schemas cannot say: that filters nest inside a
`filters` object, that empty results usually mean a missed filter rather than a
clean account, that rate limits punish a loop of `get_*`, and that
`export_dependencies_csv` hands back a URL instead of rows.

Also sharpens the SSE note. The failure is not limited to SSE-only clients — a
client that merely opens a stream after connecting fails too, and Cursor's
built-in HTTP client does exactly that.

Co-authored-by: Cursor <cursoragent@cursor.com>
`${env:CORGEA_TOKEN}` is the only route the token has into Cursor here, and the
only setup instruction was `launchctl setenv`. A Cursor started from a Windows
shortcut or a Linux desktop entry got nothing and sent an empty header.

Now says why a shell `export` cannot reach a desktop-launched editor, and gives
the persistent form for each platform. Launching Cursor from a shell that
already exports the variable is the portable check, since it settles the cause
in one step.

Same correction as Corgea/docs#327, raised there by review.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread plugins/corgea-mcp/skills/corgea-mcp/SKILL.md Outdated
The `ps` warning was written as though every client behaves like Cursor. Only
Cursor expands `${env:...}` before spawning the bridge and puts the token in
`mcp-remote`'s argv, where any user on the machine can read it. Claude Desktop
does not interpolate at all — it passes `${CORGEA_TOKEN}` through and the
bridge substitutes it from its own environment, so the token stays out of the
command line and is visible only to processes running as the same user.

Both still argue for a machine-scoped token that gets rotated; neither argues
for putting the literal back in the config.

Co-authored-by: Cursor <cursoragent@cursor.com>
@leenk7991
leenk7991 requested a review from juangaitanv August 19, 2026 08:03

@juangaitanv juangaitanv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lgtm

@leenk7991
leenk7991 merged commit f6b0d73 into main Aug 19, 2026
2 checks passed
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.

2 participants