Skip to content

fix(httpapi): percent-decode the {tool} path param before the approval lookup - #1750

Merged
Dumbris merged 1 commit into
smart-mcp-proxy:mainfrom
rudycelekli:fix/tool-param-decode-20261023-01
Oct 26, 2026
Merged

Dumbris merged 1 commit into
smart-mcp-proxy:mainfrom
rudycelekli:fix/tool-param-decode-20261023-01

Conversation

@rudycelekli

@rudycelekli rudycelekli commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Pull Request

Description

chi routes on RawPath, so path params arrive percent-encoded whenever the client's encoding differs from Go's default. MCP-1118 fixed this for {id} with decodeServerIDParam on the /servers/{id} subtree, but the nested {tool} param is still read raw by its two handlers:

  • GET /api/v1/servers/{id}/tools/{tool}/diff (handleGetToolDiff)
  • POST /api/v1/servers/{id}/tools/{tool}/enabled (handleSetToolEnabled)

Trigger. The Web UI builds both URLs with encodeURIComponent(toolName) (frontend/src/services/api.ts:577, :618), which encodes : as %3A and / as %2F. Go's default path encoding leaves : alone, so for a tool named ns:erase the request has a non-empty RawPath, chi hands the handler ns%3Aerase, and the exact-match approval lookup misses. The Review diff for that tool never loads and the per-tool enable/disable toggle fails. Tool names carrying : or / are upstream-defined and already accounted for elsewhere (see canonicalIdentity in internal/profile/glob.go).

decodeServerIDParam cannot cover {tool}: it runs as subtree middleware, before the nested route has been matched and the param populated. So the fix is the same decodePathParam call in the two handlers, as handleAddFromRegistry already does for {serverId}.

Testing

  • I have tested these changes locally
  • I have added/updated tests that prove my fix is effective or my feature works
  • All existing tests pass

TestToolSubresource_EncodedToolNameUnescaped (internal/httpapi/tool_path_param_decode_test.go) drives both routes through Server.ServeHTTP with ns%3Aerase and ns%2Ferase and asserts the decoded name reaches the lookup.

CGO_ENABLED=0 go test ./internal/httpapi -count=1                                  # ok
# with server.go reverted:
CGO_ENABLED=0 go test ./internal/httpapi -run TestToolSubresource -count=1         # FAIL, all 4 subtests
gofmt -l <changed files>                                                           # clean

(CGO_ENABLED=0 only because my local macOS SDK cannot link cgo test binaries; nothing here uses cgo.)

Note on the CodeQL alert

CodeQL reports go/reflected-xss at internal/httpapi/middleware.go:85 (statusRecorder.Write) because the decoded tool name now reaches the response. The only sinks are writeSuccess / writeError, both of which go through writeJSON: Content-Type: application/json plus json.Encoder with its default HTML escaping (<, >, & become \u003c and so on). The raw chi.URLParam value already reached the same sinks before this change. The decode only turns %3A into : so the controller lookup matches.

…l lookup

Co-authored-by: Cursor <cursoragent@cursor.com>
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Dumbris Dumbris left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Verified: the PR test fails on main for the reason described and passes on this branch merged with main; race tests for the touched packages and CI lint (bare and server tags) are clean. Also built and tested together with the rest of this batch, independently reviewed, and cross-reviewed with a second model.

@Dumbris
Dumbris merged commit b05d78e into smart-mcp-proxy:main Oct 26, 2026
48 of 49 checks passed
Dumbris pushed a commit that referenced this pull request Oct 26, 2026
…1751)

# Pull Request

## Description

Official-registry server ids are `namespace/name`
(`io.github.owner/repo`, `com.pulsemcp/google-flights`). The daemon
handles them on `/api/v1/servers/{id}/*` by decoding a `%2F`-encoded
`{id}` (`decodeServerIDParam`, MCP-1118), and `GetServerLogs` already
sends `url.PathEscape(serverName)`. Every other per-server method in
`internal/cliclient` splices the raw name into the URL with
`fmt.Sprintf`, so the slash splits the name across path segments and the
request lands on a different route (usually a 404):

```
mcpproxy upstream enable io.github.owner/repo
  -> POST /api/v1/servers/io.github.owner/repo/enable    (route: {id}=io.github.owner, then /repo/enable)
```

Affected: `ServerAction` (enable/disable/restart), `GetServerTools`,
`SetToolEnabled`, `SetAllToolsEnabled`, `TriggerOAuthLoginWithResult`,
`TriggerOAuthLogout`, `PatchServer`, `RemoveServer`, `GetToolApprovals`,
`GetToolDiff`, `ApproveTools`, `BlockTools`. Most of these declare a
local `url` variable that shadows `net/url`, which is probably how the
escape was missed. `ServerAction`'s `var url string` is renamed to
`reqURL`; the `url := fmt.Sprintf(..., url.PathEscape(...))` sites are
fine as written because the new variable's scope starts after the
statement.

Tool names in `SetToolEnabled` and `GetToolDiff` are escaped too. For a
`/` in a tool name, the daemon also has to decode `{tool}`; that is
#1750.

## Testing

- [x] I have tested these changes locally
- [x] I have added/updated tests that prove my fix is effective or my
feature works
- [x] All existing tests pass

`TestPerServerRequests_EscapeServerAndToolNames`
(`internal/cliclient/server_path_escape_test.go`) calls 12 methods
against an `httptest` server and checks `r.URL.EscapedPath()`.

```
CGO_ENABLED=0 go vet ./internal/cliclient                                       # clean
CGO_ENABLED=0 go test ./internal/cliclient -count=1                             # ok
# with client.go reverted:
CGO_ENABLED=0 go test ./internal/cliclient -run TestPerServerRequests -count=1  # FAIL, 12 of 12 cases
gofmt -l internal/cliclient                                                     # clean
```

Co-authored-by: Cursor <cursoragent@cursor.com>
@Dumbris

Dumbris commented Oct 26, 2026

Copy link
Copy Markdown
Member

Merged. Thank you, @rudycelekli! The Review diff and the per-tool enable toggle now work for tool names the Web UI percent-encodes (ns:erase). (The CodeQL reflected-XSS note on this PR is a false positive: the value only reaches application/json responses written by json.Encoder, which HTML-escapes.)

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.

3 participants