Skip to content

Commit acb80f4

Browse files
committed
Fix MCP server auth being invalidated when only URL query parameters change
1 parent cae9884 commit acb80f4

4 files changed

Lines changed: 46 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Fix MCP server auth being invalidated when only URL query parameters change.
6+
57
## 0.115.2
68

79
- Fix STDIO MCP server deadlock caused by list_changed notification handlers blocking the reader thread.

src/eca/features/tools/mcp.clj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,9 @@
342342
;; Skip OAuth entirely if Authorization header is configured
343343
has-static-auth? (some-> server-config :headers :Authorization some?)
344344
mcp-auth (get-in @db* [:mcp-auth name])
345-
;; Invalidate cached credentials when URL changed
346-
mcp-auth (when (= url (:url mcp-auth)) mcp-auth)
345+
;; Invalidate cached credentials when base URL changed (ignore query params)
346+
mcp-auth (when (= (oauth/url-without-query url)
347+
(oauth/url-without-query (:url mcp-auth))) mcp-auth)
347348
has-token? (some? (:access-token mcp-auth))
348349
token-expired? (token-expired? (:expires-at mcp-auth))
349350
;; Try to refresh if token exists but is expired

src/eca/oauth.clj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@
5151
(when (pos? (.getPort uri))
5252
(str ":" (.getPort uri))))))
5353

54+
(defn url-without-query
55+
"Strip query string and fragment from a URL, preserving scheme, authority and path.
56+
E.g., 'https://api.example.com/v1/mcp?token=x#frag' -> 'https://api.example.com/v1/mcp'
57+
Returns nil for nil input."
58+
[^String url]
59+
(when url
60+
(let [uri (java.net.URI. url)]
61+
(str (.getScheme uri) "://" (.getAuthority uri) (.getPath uri)))))
62+
5463
(defn get-free-port []
5564
(let [socket (java.net.ServerSocket. 0)
5665
port (.getLocalPort socket)]

test/eca/oauth_test.clj

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,3 +330,35 @@
330330
{:status 404}))]
331331
(let [info (oauth/oauth-info "https://example.com/mcp" nil)]
332332
(is (= oauth/eca-client-id (:client-id info)))))))
333+
334+
(deftest url-without-query-test
335+
(testing "strips query string"
336+
(is (= "https://mcp.example.com/v1"
337+
(oauth/url-without-query "https://mcp.example.com/v1?token=x&debug=true"))))
338+
339+
(testing "strips fragment"
340+
(is (= "https://mcp.example.com/v1"
341+
(oauth/url-without-query "https://mcp.example.com/v1#section"))))
342+
343+
(testing "strips both query and fragment"
344+
(is (= "https://mcp.example.com/v1"
345+
(oauth/url-without-query "https://mcp.example.com/v1?a=1#frag"))))
346+
347+
(testing "returns URL unchanged when no query or fragment"
348+
(is (= "https://mcp.example.com/v1/mcp"
349+
(oauth/url-without-query "https://mcp.example.com/v1/mcp"))))
350+
351+
(testing "preserves port"
352+
(is (= "https://mcp.example.com:8443/v1"
353+
(oauth/url-without-query "https://mcp.example.com:8443/v1?key=val"))))
354+
355+
(testing "different paths are distinct"
356+
(is (not= (oauth/url-without-query "https://mcp.example.com/v1")
357+
(oauth/url-without-query "https://mcp.example.com/v2"))))
358+
359+
(testing "different hosts are distinct"
360+
(is (not= (oauth/url-without-query "https://a.example.com/v1")
361+
(oauth/url-without-query "https://b.example.com/v1"))))
362+
363+
(testing "returns nil for nil input"
364+
(is (nil? (oauth/url-without-query nil)))))

0 commit comments

Comments
 (0)