fix(auth): reload attached credentials in live MCP
This commit is contained in:
@@ -5719,6 +5719,12 @@ def agent_scoped_folder(args):
|
|||||||
|
|
||||||
|
|
||||||
def request_json(method, path, payload=None, token_required=True, retry_auth=True, extra_headers=None):
|
def request_json(method, path, payload=None, token_required=True, retry_auth=True, extra_headers=None):
|
||||||
|
if token_required:
|
||||||
|
# `nap project attach` may run in a separate process while this MCP
|
||||||
|
# server stays alive. Reload the project auth file before building
|
||||||
|
# each authenticated request so the long-running server observes the
|
||||||
|
# replacement access and refresh tokens without a restart.
|
||||||
|
refresh_public_auth_state()
|
||||||
if token_required and not TOKEN:
|
if token_required and not TOKEN:
|
||||||
raise RuntimeError("NAPSEER_TOKEN is required")
|
raise RuntimeError("NAPSEER_TOKEN is required")
|
||||||
body = None if payload is None else json.dumps(payload).encode("utf-8")
|
body = None if payload is None else json.dumps(payload).encode("utf-8")
|
||||||
@@ -5727,6 +5733,7 @@ def request_json(method, path, payload=None, token_required=True, retry_auth=Tru
|
|||||||
headers["Authorization"] = f"Bearer {TOKEN}"
|
headers["Authorization"] = f"Bearer {TOKEN}"
|
||||||
if extra_headers:
|
if extra_headers:
|
||||||
headers.update(extra_headers)
|
headers.update(extra_headers)
|
||||||
|
request_token = TOKEN
|
||||||
request = urllib.request.Request(f"{BASE_URL}{path}", data=body, headers=headers, method=method)
|
request = urllib.request.Request(f"{BASE_URL}{path}", data=body, headers=headers, method=method)
|
||||||
try:
|
try:
|
||||||
with api_urlopen(request, timeout=HTTP_TIMEOUT_SECONDS) as response:
|
with api_urlopen(request, timeout=HTTP_TIMEOUT_SECONDS) as response:
|
||||||
@@ -5738,6 +5745,19 @@ def request_json(method, path, payload=None, token_required=True, retry_auth=Tru
|
|||||||
body_text = exc.read().decode("utf-8")
|
body_text = exc.read().decode("utf-8")
|
||||||
if exc.code == 401 and token_required and retry_auth:
|
if exc.code == 401 and token_required and retry_auth:
|
||||||
try:
|
try:
|
||||||
|
# The attach flow may have completed after this request was
|
||||||
|
# sent. Prefer its new access token over attempting to renew
|
||||||
|
# the stale in-memory refresh token that just failed.
|
||||||
|
refresh_public_auth_state()
|
||||||
|
if TOKEN and TOKEN != request_token:
|
||||||
|
return request_json(
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
payload,
|
||||||
|
token_required=True,
|
||||||
|
retry_auth=False,
|
||||||
|
extra_headers=extra_headers,
|
||||||
|
)
|
||||||
renew_auth()
|
renew_auth()
|
||||||
except Exception as renew_exc:
|
except Exception as renew_exc:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Regression tests for auth.json replacement in a running MCP wrapper."""
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import io
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def load_module():
|
||||||
|
script_path = pathlib.Path(__file__).resolve().parents[1] / "resources" / "scripts" / "napseer_mcp_server.py"
|
||||||
|
sys.path.insert(0, str(script_path.parent))
|
||||||
|
spec = importlib.util.spec_from_file_location("napseer_mcp_server_live_auth_test", script_path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
assert spec and spec.loader
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
class Response:
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *_args):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
return b'{"ok":true}'
|
||||||
|
|
||||||
|
|
||||||
|
def auth(token, refresh_token):
|
||||||
|
return {
|
||||||
|
"base_url": "https://api.example.test",
|
||||||
|
"project_id": "project-1",
|
||||||
|
"account_mode": "operator_project",
|
||||||
|
"oauth_client_id": "nap-cli",
|
||||||
|
"token": token,
|
||||||
|
"refresh_token": refresh_token,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
mod = load_module()
|
||||||
|
current = auth("fresh-access", "fresh-refresh")
|
||||||
|
requests = []
|
||||||
|
mod.load_public_auth_file = lambda: dict(current)
|
||||||
|
mod.gateway_is_unlocked = lambda: False
|
||||||
|
|
||||||
|
def open_success(request, timeout=None):
|
||||||
|
requests.append(request)
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
mod.api_urlopen = open_success
|
||||||
|
mod.TOKEN = "stale-access"
|
||||||
|
mod.REFRESH_TOKEN = "stale-refresh"
|
||||||
|
result = mod.request_json("GET", "/v1/projects/project-1")
|
||||||
|
assert result == {"ok": True}
|
||||||
|
assert requests[-1].get_header("Authorization") == "Bearer fresh-access"
|
||||||
|
assert mod.REFRESH_TOKEN == "fresh-refresh"
|
||||||
|
|
||||||
|
current = auth("stale-access", "stale-refresh")
|
||||||
|
requests.clear()
|
||||||
|
renew_calls = []
|
||||||
|
mod.renew_auth = lambda: renew_calls.append(True)
|
||||||
|
|
||||||
|
def attach_during_request(request, timeout=None):
|
||||||
|
requests.append(request)
|
||||||
|
if len(requests) == 1:
|
||||||
|
current.update(auth("attached-access", "attached-refresh"))
|
||||||
|
raise urllib.error.HTTPError(
|
||||||
|
request.full_url,
|
||||||
|
401,
|
||||||
|
"Unauthorized",
|
||||||
|
{},
|
||||||
|
io.BytesIO(b'{"error":{"code":"unauthorized"}}'),
|
||||||
|
)
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
mod.api_urlopen = attach_during_request
|
||||||
|
result = mod.request_json("GET", "/v1/projects/project-1")
|
||||||
|
assert result == {"ok": True}
|
||||||
|
assert len(requests) == 2
|
||||||
|
assert requests[0].get_header("Authorization") == "Bearer stale-access"
|
||||||
|
assert requests[1].get_header("Authorization") == "Bearer attached-access"
|
||||||
|
assert renew_calls == []
|
||||||
|
|
||||||
|
print("ok: running MCP wrapper reloads replaced auth state")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
Reference in New Issue
Block a user