Repository navigation
fix(inventory): do not require Mcp-Param headers on app-only tools - #3854
Shanmukh-l33t wants to merge 2 commits into
Conversation
ui_get declares `_meta.ui.visibility: ["app"]`, so its only callers are MCP App views. Views reach the server through the host's tools/call proxy and App.callServerTool posts JSON-RPC over the MCP Apps bridge, which gives a view no way to set HTTP request headers. Because the generic owner/repo projection in AnnotateHeaderParams annotated ui_get too, the go-sdk Streamable HTTP boundary rejected every view lookup on protocol 2026-07-28 with `-32020 header mismatch: missing Mcp-Param-owner header`, which is what the issue_write form surfaced while loading labels, assignees, milestones, issue types and custom fields. Skip the annotation for tool definitions whose ui.visibility excludes "model". Owner and repo stay in the call arguments for those tools, and model-visible tools keep the projection requirement unchanged.
There was a problem hiding this comment.
🟡 Changes recommended
App-only tools registered with inferred input schemas and concrete output types still receive x-mcp-header bindings, causing headerless app calls to be rejected.
1 open issue
What changed in this PR
This PR addresses #3834 by exempting app-only tools such as ui_get from required Mcp-Param-* headers, allowing form metadata lookups through hosts that do not project those headers.
Changes:
- Reuses exported visibility detection to skip header annotations for app-only tools.
- Adds unit and HTTP regression coverage while preserving model-visible header requirements.
- Documents the app-only exception.
| File | Description |
|---|---|
pkg/inventory/server_tool.go |
Skips header annotations for app-only tools. |
pkg/inventory/header_params_test.go |
Tests app-only exemption and model-visible annotations. |
pkg/inventory/builder.go |
Exports the existing app-only visibility helper. |
pkg/http/app_tool_headers_test.go |
Tests HTTP calls with and without projected headers. |
pkg/github/header_params_test.go |
Checks the exemption across the GitHub tool inventory. |
docs/streamable-http.md |
Documents body-based routing for app-only calls. |
Review effort: Balanced (dynamic)
| if IsAppOnlyTool(tool) { | ||
| return | ||
| } |
An app-only tool that declares its input schema is already skipped by AnnotateHeaderParams. One that lets the SDK infer the schema from its input type was not: prepareTyped asks CachedInputSchemaFor, which annotates the schema while building the shared per-type cache entry, where no tool metadata is available to consult. Cache the unannotated form alongside the annotated one and pick by visibility when the schema is handed to a tool, so an app-only tool and a model-visible tool that share an input type each get the right projection. The regression test registers both tools over one input type and asserts the app-only schema carries no x-mcp-header while the model-visible twin keeps owner and repo. Refs github#3834
|
Good catch, and it is a real hole rather than a stylistic one. Confirmed at source on a915221: Fixed in 62e2188 by making the schema selection visibility aware:
Regression test added, Red and green, run locally:
The test also asserts the arguments keep |

Problem
While the
issue_writeMCP App form loads in VS Code, the form shows:It happens "on almost every form we open" and clears on retry. Fixes #3834.
Root cause
Two facts combine:
ui/src/hooks/useMcpApp.ts:97callsapp.callServerTool({ name, arguments: args }).App.callServerToolposts a JSON-RPCtools/callover the MCP AppsPostMessageTransportand the host replays it against the server. A view has no API for HTTP request headers, so no code inui/can projectMcp-Param-*values.pkg/inventory/server_tool.go:290AnnotateHeaderParamsannotatedownerandrepowithx-mcp-headeron every tool that has those properties, includingui_get.ui_getdeclares_meta.ui.visibility: ["app"](pkg/github/ui_tools.go:47), so views are its only callers.The go-sdk Streamable HTTP boundary then requires the header whenever the argument is present (
go-sdk v1.8.0 mcp/streamable_headers.go:359validateMcpHeaders,:402validateParamHeaders) forMcp-Protocol-Version >= 2026-07-28. Any host that does not project headers for proxied app calls therefore gets a hard-32020on every view lookup: labels (ui/src/apps/issue-write/App.tsx:550), assignees (:577), milestones (:603), issue types (:631), issue fields (:659).So the header requirement on
ui_getwas unsatisfiable for the only caller class that can reach it. Theissue_typesdetail in the report is a red herring:issue_typeslegitimately sends onlyowner, andrepostays unannotated for that call because the annotation only applies to a non-null argument.What changed
pkg/inventory/server_tool.goAnnotateHeaderParamsnow returns early for tools whoseui.visibilityexcludes"model", using the existing visibility logic exposed asinventory.IsAppOnlyTool(pkg/inventory/builder.go:401). Three consequences, all intended:ui_getis no longer annotated intools/list, so the SDK does not reject its calls when the header is absent.owner/reporemain in the call arguments, so the handler and any proxy can still read them from the body.docs/streamable-http.mdrecords the exception. No tool names, schemas, UI, or dependencies change, so the generated README catalog is unaffected (verified below).Tests
pkg/http/app_tool_headers_test.godrives the productionNewHTTPMcpHandleraround the real GitHub inventory atMcp-Protocol-Version: 2026-07-28and asserts four cases: app-only call without headers returns 200, app-only call with headers returns 200,issue_writewithout headers returns 400-32020,issue_writewith headers returns 200.Red at the parent commit
0663b83(fix removed, test files kept):go test ./pkg/http/ -run 'AppOnly' -count=1 -vSubtest
app-only_tool_without_projected_headers_reaches_the_handlerfails:expected: 200,actual: 400, body{"jsonrpc":"2.0","id":1,"error":{"code":-32020,"message":"header mismatch: missing Mcp-Param-owner header for parameter \"owner\""}}, the exact string from the issue. The other three subtests pass, so the failure is specific to the app-only exemption.go test ./pkg/inventory/ -run 'TestAnnotateHeaderParamsSkipsAppOnlyTools' -count=1 -vFails with
Expected nil, but got: map[string]interface {}{"x-mcp-header":"owner"}.TestAllToolsRoutingParamsGetHeadersinpkg/githubcannot be red at the parent commit: it calls the newly exportedinventory.IsAppOnlyTool, so on unmodifiedmainit fails to build withundefined: inventory.IsAppOnlyTool. Its new assertion (app-only tools must stay unannotated) is only meaningful together with the export, and it passes with this change.Green with the fix:
go build ./...: clean.go test ./pkg/inventory/... ./pkg/github/ -run 'Header' -count=1:ok pkg/inventory 0.110s,ok pkg/github 0.042s.go test ./pkg/http/ -run 'AppOnly' -count=1 -v:PASS, all 4 subtests,ok pkg/http 0.125s.go test ./pkg/inventory/... ./pkg/http/... ./pkg/github/ -count=1 -p 2:okforpkg/inventory(0.471s),pkg/http(0.896s),pkg/http/headers(0.003s),pkg/http/middleware(0.005s),pkg/http/oauth(0.022s),pkg/http/servercard(0.003s),pkg/http/transport(0.011s) andpkg/github(19.847s);pkg/http/markhas no test files. 0 failures.gofmt -s -lover the five changed Go files: no output.go vet ./pkg/inventory/... ./pkg/http/... ./pkg/github/: clean.golangci-lint run ./pkg/inventory/... ./pkg/http/ ./pkg/github/(v2.14.0):0 issues.go run ./cmd/github-mcp-server generate-docs:git statusafterwards is empty, so the generated docs and README catalog already match this branch and no regenerated diff is missing.MCP impact
No tool is added, removed, renamed, or reshaped.
ui_getstops advertisingx-mcp-headeronowner/repo; model-visible tools and their header contract are byte for byte unchanged.Security / limits
The exemption applies only to tools whose declared
_meta.ui.visibilityexcludes"model". Across the full tool inventory (136 tools with all toolsets enabled) that isui_getalone, measured on this branch. It removes a routing hint, not an authorization check: scope challenges, availability guards, repository routing and body validation are untouched, and the arguments still carryowner/repofor the proxy.Tool renaming
Lint & tests
golangci-lint runon the changed packages (0 issues)./script/test(the full-racesweep was not run to keep load off shared hardware; the affected packages were run in full, as listed above)Docs
docs/streamable-http.mddocuments the app-only exception.script/generate-docsproduced no further diff.Skills
No tool or toolset instruction changed, so no bundled skill text or instruction snapshot moves;
generate-docsconfirmed the catalog is identical.Scope note
This removes the error for the form's own lookups. A view-initiated call to a model-visible tool (
issue_writesubmit from the form) is still subject to header projection, so hosts that skip projection for proxied calls can still surface-32020there once and recover on retry; that path is the host's projection gap, not something the tool contract can satisfy on the view's behalf.