archery-sql-mcp 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: archery-sql-mcp
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Archery SQL 查询 MCP 工具:自动登录 + 会话自愈 + 只读查询,供任意 MCP 客户端与 CLI 使用
5
5
  License: MIT
6
6
  Requires-Python: >=3.10
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "archery-sql-mcp"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "Archery SQL 查询 MCP 工具:自动登录 + 会话自愈 + 只读查询,供任意 MCP 客户端与 CLI 使用"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -28,7 +28,7 @@ from typing import Any, Optional
28
28
 
29
29
  import httpx
30
30
 
31
- VERSION = "0.2.0"
31
+ VERSION = "0.2.2"
32
32
 
33
33
  # ── 配置文件与会话缓存位置(可用 ARCHERY_CONFIG_DIR 整体改根目录) ──────────
34
34
  _CONFIG_DIR = Path(os.environ.get("ARCHERY_CONFIG_DIR", str(Path.home() / ".archery-mcp")))
@@ -166,15 +166,26 @@ class SessionManager:
166
166
  },
167
167
  headers={"Referer": f"{self.base}/login/"},
168
168
  )
169
- # 成功:302 跳往非 /login 路径;失败:302 回 /login 或 200 带错误页
170
- location = resp.headers.get("location", "")
171
- if resp.status_code not in (301, 302) or "/login" in location:
172
- reason = _extract_login_error(resp.text) or (
173
- f"HTTP {resp.status_code}"
174
- + (f" → {location}" if location else "")
175
- + "(用户名/密码错误,或账号被禁用)"
176
- )
177
- raise ArcheryError(f"Archery 登录失败: {reason}")
169
+ # /authenticate/ 是 ajax JSON 接口:恒为 HTTP 200,
170
+ # {"status": 0}=成功(sessionid 经 Set-Cookie 下发),status=1 时 msg 为可读错误。
171
+ # 兼容旧式表单行为(302 跳转)作为回退分支。
172
+ payload: Any = None
173
+ try:
174
+ payload = resp.json()
175
+ except Exception:
176
+ payload = None
177
+ if payload is not None:
178
+ if payload.get("status") != 0:
179
+ raise ArcheryError(f"Archery 登录失败: {payload.get('msg') or payload}")
180
+ else:
181
+ location = resp.headers.get("location", "")
182
+ if resp.status_code not in (301, 302) or "/login" in location:
183
+ reason = _extract_login_error(resp.text) or (
184
+ f"HTTP {resp.status_code}"
185
+ + (f" → {location}" if location else "")
186
+ + "(用户名/密码错误,或账号被禁用)"
187
+ )
188
+ raise ArcheryError(f"Archery 登录失败: {reason}")
178
189
 
179
190
  self.sessionid = client.cookies.get("sessionid")
180
191
  self.csrftoken = client.cookies.get("csrftoken") or cookie_csrf
@@ -248,13 +259,13 @@ class ArcheryClient:
248
259
  for _attempt in range(2):
249
260
  await self.sm.ensure()
250
261
  async with httpx.AsyncClient(
251
- timeout=self.cfg["request_timeout_s"], follow_redirects=False
262
+ timeout=self.cfg["request_timeout_s"], follow_redirects=False,
263
+ cookies={"csrftoken": self.sm.csrftoken or "",
264
+ "sessionid": self.sm.sessionid or ""},
252
265
  ) as client:
253
266
  resp = await client.request(
254
267
  method, f"{self.sm.base}{path}",
255
268
  data=data, params=params,
256
- cookies={"csrftoken": self.sm.csrftoken or "",
257
- "sessionid": self.sm.sessionid or ""},
258
269
  headers={"X-CSRFToken": self.sm.csrftoken or "",
259
270
  "Referer": self.sm.base + "/"},
260
271
  )
@@ -306,9 +317,28 @@ class ArcheryClient:
306
317
  raise ArcheryError(f"/query/ 返回非 JSON (HTTP {resp.status_code}),请联系管理员确认平台版本兼容性")
307
318
  if submitted.get("status") != 0:
308
319
  raise ArcheryError(f"/query/ 提交失败: {submitted.get('msg') or submitted}")
309
- query_id = (submitted.get("data") or {}).get("query_id")
320
+ data = submitted.get("data") or {}
321
+
322
+ def _finish(columns: Any, rows: Any) -> dict[str, Any]:
323
+ return {
324
+ "status": "finished",
325
+ "columns": columns or [],
326
+ "rows": rows or [],
327
+ "row_count": len(rows or []),
328
+ "elapsed_ms": int((time.monotonic() - started) * 1000),
329
+ }
330
+
331
+ # 同步模式(实测 2026-08-28,公司 Archery 版本):
332
+ # /query/ 直接返回 column_list / rows / error,无 query_id。
333
+ if data.get("column_list") is not None or data.get("rows") is not None:
334
+ if data.get("error"):
335
+ raise ArcheryError(f"SQL 执行错误: {data['error']}")
336
+ return _finish(data.get("column_list"), data.get("rows"))
337
+
338
+ # 异步模式(部分 Archery 版本/配置):提交返回 query_id → 轮询 /queryresult/。
339
+ query_id = data.get("query_id")
310
340
  if not query_id:
311
- raise ArcheryError(f"/query/ 未返回 query_id: {submitted}")
341
+ raise ArcheryError(f"/query/ 未返回结果也未返回 query_id: {submitted}")
312
342
 
313
343
  # 轮询至终态
314
344
  interval = float(self.cfg["poll_interval_s"])