@josephyan/qingflow-app-user-mcp 0.2.0-beta.983 → 0.2.0-beta.984

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.
package/README.md CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.983
6
+ npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.984
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.983 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.984 qingflow-app-user-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-user-mcp",
3
- "version": "0.2.0-beta.983",
3
+ "version": "0.2.0-beta.984",
4
4
  "description": "Operational end-user MCP for Qingflow records, tasks, comments, and directory workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "0.2.0b983"
7
+ version = "0.2.0b984"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
 
6
6
  __all__ = ["__version__"]
7
7
 
8
- _FALLBACK_VERSION = "0.2.0b983"
8
+ _FALLBACK_VERSION = "0.2.0b984"
9
9
 
10
10
 
11
11
  def _resolve_local_pyproject_version() -> str | None:
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import os
4
5
  from dataclasses import asdict, dataclass
5
6
  from datetime import datetime, timezone
6
7
  from pathlib import Path
@@ -70,6 +71,7 @@ class SessionStore:
70
71
  profiles_path = get_profiles_path() if base_dir is None else Path(base_dir) / "profiles.json"
71
72
  self._profiles_path = profiles_path
72
73
  self._profiles_path.parent.mkdir(parents=True, exist_ok=True)
74
+ self._secrets_path = self._profiles_path.parent / "secrets.json"
73
75
  self._keyring = keyring_backend if keyring_backend is not None else keyring
74
76
  self._memory_sessions: dict[str, BackendSession] = {}
75
77
  self._logged_out_profiles: set[str] = set()
@@ -264,26 +266,78 @@ class SessionStore:
264
266
  json.dump(payload, handle, ensure_ascii=False, indent=2)
265
267
 
266
268
  def _set_secret(self, key: str, value: str) -> bool:
267
- if self._keyring is None:
268
- return False
269
+ if self._keyring is not None:
270
+ try:
271
+ self._keyring.set_password(KEYRING_SERVICE_NAME, key, value)
272
+ self._delete_file_secret(key)
273
+ return True
274
+ except Exception:
275
+ pass
276
+ return self._set_file_secret(key, value)
277
+
278
+ def _get_secret(self, key: str) -> str | None:
279
+ if self._keyring is not None:
280
+ try:
281
+ value = self._keyring.get_password(KEYRING_SERVICE_NAME, key)
282
+ except Exception:
283
+ value = None
284
+ if value:
285
+ return value
286
+ return self._get_file_secret(key)
287
+
288
+ def _delete_secret(self, key: str) -> None:
289
+ if self._keyring is not None:
290
+ try:
291
+ self._keyring.delete_password(KEYRING_SERVICE_NAME, key)
292
+ except Exception:
293
+ pass
294
+ self._delete_file_secret(key)
295
+
296
+ def _load_file_secrets(self) -> dict[str, str]:
297
+ if not self._secrets_path.exists():
298
+ return {}
299
+ try:
300
+ with self._secrets_path.open("r", encoding="utf-8") as handle:
301
+ payload = json.load(handle)
302
+ except (OSError, json.JSONDecodeError):
303
+ return {}
304
+ if not isinstance(payload, dict):
305
+ return {}
306
+ return {str(key): str(value) for key, value in payload.items() if isinstance(value, str)}
307
+
308
+ def _save_file_secrets(self, payload: dict[str, str]) -> bool:
309
+ self._secrets_path.parent.mkdir(parents=True, exist_ok=True)
269
310
  try:
270
- self._keyring.set_password(KEYRING_SERVICE_NAME, key, value)
311
+ fd = os.open(self._secrets_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
312
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
313
+ json.dump(payload, handle, ensure_ascii=False, indent=2)
314
+ try:
315
+ os.chmod(self._secrets_path, 0o600)
316
+ except OSError:
317
+ pass
271
318
  return True
272
- except Exception:
319
+ except OSError:
273
320
  return False
274
321
 
275
- def _get_secret(self, key: str) -> str | None:
276
- if self._keyring is None:
277
- return None
278
- try:
279
- return self._keyring.get_password(KEYRING_SERVICE_NAME, key)
280
- except Exception:
281
- return None
322
+ def _set_file_secret(self, key: str, value: str) -> bool:
323
+ payload = self._load_file_secrets()
324
+ payload[key] = value
325
+ return self._save_file_secrets(payload)
282
326
 
283
- def _delete_secret(self, key: str) -> None:
284
- if self._keyring is None:
327
+ def _get_file_secret(self, key: str) -> str | None:
328
+ return self._load_file_secrets().get(key)
329
+
330
+ def _delete_file_secret(self, key: str) -> None:
331
+ payload = self._load_file_secrets()
332
+ if key not in payload:
333
+ return
334
+ payload.pop(key, None)
335
+ if payload:
336
+ self._save_file_secrets(payload)
285
337
  return
286
338
  try:
287
- self._keyring.delete_password(KEYRING_SERVICE_NAME, key)
288
- except Exception:
339
+ self._secrets_path.unlink()
340
+ except FileNotFoundError:
341
+ return
342
+ except OSError:
289
343
  return