nexotype-mcp 0.1.0__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.
@@ -0,0 +1,176 @@
1
+ Metadata-Version: 2.4
2
+ Name: nexotype-mcp
3
+ Version: 0.1.0
4
+ Summary: Nexotype MCP Server — biomedical knowledge graph for drug discovery, genomics, and market intelligence
5
+ Keywords: mcp,nexotype,biotech,drug-discovery,knowledge-graph,genomics,ai,claude,llm
6
+ Author: Robert Radoslav
7
+ Author-email: Robert Radoslav <43938206+rbtrsv@users.noreply.github.com>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Healthcare Industry
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Dist: click>=8.3.2
16
+ Requires-Dist: fastmcp>=3.2.3
17
+ Requires-Dist: httpx>=0.28.1
18
+ Requires-Python: >=3.12
19
+ Project-URL: Documentation, https://www.nexotype.com
20
+ Project-URL: Homepage, https://www.nexotype.com
21
+ Project-URL: PyPI, https://pypi.org/project/nexotype-mcp/
22
+ Description-Content-Type: text/markdown
23
+
24
+ # Nexotype MCP Server
25
+
26
+ AI-powered biomedical knowledge graph for drug discovery, genomics, and market intelligence.
27
+
28
+ Connect your AI tools to the Nexotype platform and query the biomedical knowledge graph through conversation.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install nexotype-mcp
34
+ ```
35
+
36
+ Or with uv:
37
+
38
+ ```bash
39
+ uv tool install nexotype-mcp
40
+ ```
41
+
42
+ Already installed? Update:
43
+
44
+ ```bash
45
+ pip install --upgrade nexotype-mcp
46
+ ```
47
+
48
+ ## Authentication
49
+
50
+ ```bash
51
+ nexotype-cli login
52
+ ```
53
+
54
+ Enter your email and password. Credentials are stored securely at `~/.nexotype/credentials.json` (chmod 600).
55
+
56
+ ## Configure Your AI Client
57
+
58
+ ### Claude Desktop
59
+
60
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
61
+
62
+ ```json
63
+ {
64
+ "mcpServers": {
65
+ "nexotype": {
66
+ "command": "nexotype-mcp"
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ ### Claude Code
73
+
74
+ ```bash
75
+ claude mcp add nexotype -- nexotype-mcp
76
+ ```
77
+
78
+ ### VS Code / Cursor
79
+
80
+ Add to your MCP settings:
81
+
82
+ ```json
83
+ {
84
+ "nexotype": {
85
+ "command": "nexotype-mcp"
86
+ }
87
+ }
88
+ ```
89
+
90
+ Restart the application after saving.
91
+
92
+ ## Tools (28)
93
+
94
+ ### Graph Intelligence (6 tools)
95
+
96
+ | Tool | Description |
97
+ |------|-------------|
98
+ | `explore_network` | N-hop neighborhood of any entity |
99
+ | `find_path` | Shortest path between two entities |
100
+ | `find_similar` | Entities sharing the most relationships |
101
+ | `company_deep_dive` | Full portfolio of a pharma/biotech company |
102
+ | `drug_discovery` | Targets, pathways, indications for a drug |
103
+ | `competitive_landscape` | All drugs and companies targeting a protein |
104
+
105
+ ### AI Query (1 tool)
106
+
107
+ | Tool | Description |
108
+ |------|-------------|
109
+ | `ask_knowledge_graph` | Natural language question → grounded answer from graph |
110
+
111
+ ### Search & Browse (2 tools)
112
+
113
+ | Tool | Description |
114
+ |------|-------------|
115
+ | `search_entities` | List entities of any of the 61 types |
116
+ | `get_entity` | Full details for a specific entity |
117
+
118
+ ### Context (3 tools)
119
+
120
+ | Tool | Description |
121
+ |------|-------------|
122
+ | `get_permissions` | Check subscription tier and domain access |
123
+ | `list_subjects` | List available biological subjects |
124
+ | `set_subject` | Set active subject for personalization tools |
125
+
126
+ ### Personalized Health (10 tools)
127
+
128
+ | Tool | Description |
129
+ |------|-------------|
130
+ | `get_user_profile` | Personal info linked to the subject |
131
+ | `list_user_variants` | Detected genomic variants |
132
+ | `list_biomarker_readings` | Time-series health data |
133
+ | `create_biomarker_reading` | Log a new biomarker measurement |
134
+ | `list_treatment_logs` | Intervention history |
135
+ | `create_treatment_log` | Log a new treatment |
136
+ | `list_pathway_scores` | Pathway health scores |
137
+ | `list_recommendations` | Personalized treatment suggestions |
138
+ | `upload_genomic_file` | Upload VCF/23andMe file for analysis |
139
+ | `get_processing_status` | Check genomic file processing progress |
140
+
141
+ ### Commercial Intelligence (6 tools)
142
+
143
+ | Tool | Description |
144
+ |------|-------------|
145
+ | `list_market_organizations` | Pharma/biotech companies |
146
+ | `get_market_organization` | Full company details |
147
+ | `list_patents` | Patent filings and status |
148
+ | `list_development_pipelines` | Clinical trial phases |
149
+ | `list_regulatory_approvals` | FDA/EMA approvals |
150
+ | `list_licensing_agreements` | Commercial partnerships |
151
+
152
+ ## Example Queries
153
+
154
+ ```
155
+ "What drugs target EGFR and are in Phase III?"
156
+ "Show me Novo Nordisk's full portfolio"
157
+ "What variants are associated with Alzheimer's risk?"
158
+ "Compare GLP-1 receptor agonists across companies"
159
+ "Who else is targeting mTOR?"
160
+ "List all therapeutic peptides in the database"
161
+ "What are my detected genomic variants?"
162
+ ```
163
+
164
+ ## CLI Commands
165
+
166
+ | Command | Description |
167
+ |---------|-------------|
168
+ | `nexotype-cli login` | Authenticate with email/password |
169
+ | `nexotype-cli logout` | Delete stored credentials |
170
+ | `nexotype-cli status` | Show current auth status |
171
+
172
+ ## Environment Variables
173
+
174
+ | Variable | Default | Description |
175
+ |----------|---------|-------------|
176
+ | `NEXOTYPE_API_URL` | `http://localhost:8000` | API server URL |
@@ -0,0 +1,153 @@
1
+ # Nexotype MCP Server
2
+
3
+ AI-powered biomedical knowledge graph for drug discovery, genomics, and market intelligence.
4
+
5
+ Connect your AI tools to the Nexotype platform and query the biomedical knowledge graph through conversation.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install nexotype-mcp
11
+ ```
12
+
13
+ Or with uv:
14
+
15
+ ```bash
16
+ uv tool install nexotype-mcp
17
+ ```
18
+
19
+ Already installed? Update:
20
+
21
+ ```bash
22
+ pip install --upgrade nexotype-mcp
23
+ ```
24
+
25
+ ## Authentication
26
+
27
+ ```bash
28
+ nexotype-cli login
29
+ ```
30
+
31
+ Enter your email and password. Credentials are stored securely at `~/.nexotype/credentials.json` (chmod 600).
32
+
33
+ ## Configure Your AI Client
34
+
35
+ ### Claude Desktop
36
+
37
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
38
+
39
+ ```json
40
+ {
41
+ "mcpServers": {
42
+ "nexotype": {
43
+ "command": "nexotype-mcp"
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ ### Claude Code
50
+
51
+ ```bash
52
+ claude mcp add nexotype -- nexotype-mcp
53
+ ```
54
+
55
+ ### VS Code / Cursor
56
+
57
+ Add to your MCP settings:
58
+
59
+ ```json
60
+ {
61
+ "nexotype": {
62
+ "command": "nexotype-mcp"
63
+ }
64
+ }
65
+ ```
66
+
67
+ Restart the application after saving.
68
+
69
+ ## Tools (28)
70
+
71
+ ### Graph Intelligence (6 tools)
72
+
73
+ | Tool | Description |
74
+ |------|-------------|
75
+ | `explore_network` | N-hop neighborhood of any entity |
76
+ | `find_path` | Shortest path between two entities |
77
+ | `find_similar` | Entities sharing the most relationships |
78
+ | `company_deep_dive` | Full portfolio of a pharma/biotech company |
79
+ | `drug_discovery` | Targets, pathways, indications for a drug |
80
+ | `competitive_landscape` | All drugs and companies targeting a protein |
81
+
82
+ ### AI Query (1 tool)
83
+
84
+ | Tool | Description |
85
+ |------|-------------|
86
+ | `ask_knowledge_graph` | Natural language question → grounded answer from graph |
87
+
88
+ ### Search & Browse (2 tools)
89
+
90
+ | Tool | Description |
91
+ |------|-------------|
92
+ | `search_entities` | List entities of any of the 61 types |
93
+ | `get_entity` | Full details for a specific entity |
94
+
95
+ ### Context (3 tools)
96
+
97
+ | Tool | Description |
98
+ |------|-------------|
99
+ | `get_permissions` | Check subscription tier and domain access |
100
+ | `list_subjects` | List available biological subjects |
101
+ | `set_subject` | Set active subject for personalization tools |
102
+
103
+ ### Personalized Health (10 tools)
104
+
105
+ | Tool | Description |
106
+ |------|-------------|
107
+ | `get_user_profile` | Personal info linked to the subject |
108
+ | `list_user_variants` | Detected genomic variants |
109
+ | `list_biomarker_readings` | Time-series health data |
110
+ | `create_biomarker_reading` | Log a new biomarker measurement |
111
+ | `list_treatment_logs` | Intervention history |
112
+ | `create_treatment_log` | Log a new treatment |
113
+ | `list_pathway_scores` | Pathway health scores |
114
+ | `list_recommendations` | Personalized treatment suggestions |
115
+ | `upload_genomic_file` | Upload VCF/23andMe file for analysis |
116
+ | `get_processing_status` | Check genomic file processing progress |
117
+
118
+ ### Commercial Intelligence (6 tools)
119
+
120
+ | Tool | Description |
121
+ |------|-------------|
122
+ | `list_market_organizations` | Pharma/biotech companies |
123
+ | `get_market_organization` | Full company details |
124
+ | `list_patents` | Patent filings and status |
125
+ | `list_development_pipelines` | Clinical trial phases |
126
+ | `list_regulatory_approvals` | FDA/EMA approvals |
127
+ | `list_licensing_agreements` | Commercial partnerships |
128
+
129
+ ## Example Queries
130
+
131
+ ```
132
+ "What drugs target EGFR and are in Phase III?"
133
+ "Show me Novo Nordisk's full portfolio"
134
+ "What variants are associated with Alzheimer's risk?"
135
+ "Compare GLP-1 receptor agonists across companies"
136
+ "Who else is targeting mTOR?"
137
+ "List all therapeutic peptides in the database"
138
+ "What are my detected genomic variants?"
139
+ ```
140
+
141
+ ## CLI Commands
142
+
143
+ | Command | Description |
144
+ |---------|-------------|
145
+ | `nexotype-cli login` | Authenticate with email/password |
146
+ | `nexotype-cli logout` | Delete stored credentials |
147
+ | `nexotype-cli status` | Show current auth status |
148
+
149
+ ## Environment Variables
150
+
151
+ | Variable | Default | Description |
152
+ |----------|---------|-------------|
153
+ | `NEXOTYPE_API_URL` | `http://localhost:8000` | API server URL |
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "nexotype-mcp"
3
+ version = "0.1.0"
4
+ description = "Nexotype MCP Server — biomedical knowledge graph for drug discovery, genomics, and market intelligence"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "Robert Radoslav", email = "43938206+rbtrsv@users.noreply.github.com" }
9
+ ]
10
+ requires-python = ">=3.12"
11
+ keywords = ["mcp", "nexotype", "biotech", "drug-discovery", "knowledge-graph", "genomics", "ai", "claude", "llm"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Healthcare Industry",
15
+ "Intended Audience :: Science/Research",
16
+ "Topic :: Scientific/Engineering :: Bio-Informatics",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ ]
20
+ dependencies = [
21
+ "click>=8.3.2",
22
+ "fastmcp>=3.2.3",
23
+ "httpx>=0.28.1",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://www.nexotype.com"
28
+ Documentation = "https://www.nexotype.com"
29
+ "PyPI" = "https://pypi.org/project/nexotype-mcp/"
30
+
31
+ [project.scripts]
32
+ nexotype-mcp = "nexotype_mcp.server:main"
33
+ nexotype-cli = "nexotype_mcp.cli:main"
34
+
35
+ [build-system]
36
+ requires = ["uv_build>=0.8.15,<0.9.0"]
37
+ build-backend = "uv_build"
@@ -0,0 +1 @@
1
+ # Hello from nexotype-mcp!
@@ -0,0 +1,102 @@
1
+ """
2
+ Nexotype CLI — Interactive terminal authentication
3
+
4
+ Commands:
5
+ nexotype-cli login — Login with email/password in terminal
6
+ nexotype-cli logout — Delete stored credentials
7
+ nexotype-cli status — Show current auth status
8
+ """
9
+
10
+ import getpass
11
+
12
+ import click
13
+ import httpx
14
+
15
+ from .config import API_BASE_URL, REQUEST_TIMEOUT
16
+ from .credentials import load_credentials, save_credentials, delete_credentials
17
+
18
+
19
+ @click.group()
20
+ def main():
21
+ """Nexotype CLI — Authenticate and manage your Nexotype MCP connection."""
22
+ pass
23
+
24
+
25
+ def _save_and_show(data: dict) -> None:
26
+ """Save credentials and show orgs. Shared between login flows."""
27
+ token = data["token"]
28
+ user = data["data"]["user"]
29
+ orgs = data["data"]["organizations"]
30
+
31
+ # Default to first organization
32
+ default_org_id = orgs[0]["id"] if orgs else None
33
+
34
+ save_credentials({
35
+ "access_token": token["access_token"],
36
+ "refresh_token": token["refresh_token"],
37
+ "organization_id": default_org_id,
38
+ "user_email": user["email"],
39
+ })
40
+
41
+ click.echo(f"\n✓ Logged in as {user['email']}")
42
+ click.echo(f"✓ Credentials saved to ~/.nexotype/credentials.json\n")
43
+
44
+ if orgs:
45
+ click.echo("Organizations:")
46
+ for org in orgs:
47
+ marker = " (active)" if org["id"] == default_org_id else ""
48
+ click.echo(f" {org['id']}. {org['name']} — role: {org['user_role']}{marker}")
49
+ else:
50
+ click.echo("Warning: No organizations found. Create one in the Nexotype dashboard.")
51
+
52
+
53
+ @main.command()
54
+ def login():
55
+ """Login to Nexotype with email and password."""
56
+ email = click.prompt("Email")
57
+ password = getpass.getpass("Password: ")
58
+
59
+ click.echo("Logging in...")
60
+
61
+ try:
62
+ with httpx.Client(timeout=REQUEST_TIMEOUT) as client:
63
+ r = client.post(
64
+ f"{API_BASE_URL}/accounts/auth/login",
65
+ json={"email": email, "password": password},
66
+ )
67
+
68
+ if r.status_code == 401:
69
+ click.echo("Error: Invalid email or password.", err=True)
70
+ raise SystemExit(1)
71
+
72
+ r.raise_for_status()
73
+ _save_and_show(r.json())
74
+
75
+ except httpx.HTTPStatusError as e:
76
+ click.echo(f"Error: API returned {e.response.status_code}", err=True)
77
+ raise SystemExit(1)
78
+ except httpx.ConnectError:
79
+ click.echo(f"Error: Cannot connect to {API_BASE_URL}. Is the server running?", err=True)
80
+ raise SystemExit(1)
81
+
82
+
83
+ @main.command()
84
+ def logout():
85
+ """Delete stored credentials."""
86
+ delete_credentials()
87
+ click.echo("✓ Logged out. Credentials deleted.")
88
+
89
+
90
+ @main.command()
91
+ def status():
92
+ """Show current authentication status."""
93
+ creds = load_credentials()
94
+ if not creds:
95
+ click.echo("Not authenticated. Run 'nexotype-cli login' first.")
96
+ return
97
+
98
+ click.echo(f"Email: {creds.get('user_email', 'unknown')}")
99
+ click.echo(f"Organization ID: {creds.get('organization_id', 'not set')}")
100
+ click.echo(f"API: {API_BASE_URL}")
101
+ click.echo(f"Token: {'present' if creds.get('access_token') else 'missing'}")
102
+ click.echo(f"Refresh token: {'present' if creds.get('refresh_token') else 'missing'}")
@@ -0,0 +1,143 @@
1
+ """
2
+ Nexotype MCP Server — HTTP Client
3
+
4
+ Async httpx wrapper that handles:
5
+ - JWT authentication (from ~/.nexotype/credentials.json)
6
+ - Subject context (for personalization tools)
7
+ - Automatic token refresh on 401 responses
8
+ - Multipart file upload for VCF files
9
+
10
+ Key difference from Finpy: Nexotype backend resolves organization from JWT,
11
+ so no organization_id query param is needed.
12
+ """
13
+
14
+ import httpx
15
+
16
+ from .config import API_BASE_URL, REQUEST_TIMEOUT, UPLOAD_TIMEOUT
17
+ from .credentials import load_credentials, save_credentials
18
+
19
+
20
+ class NexotypeClient:
21
+ """HTTP client for Nexotype API with JWT auth and subject context."""
22
+
23
+ def __init__(self) -> None:
24
+ self.base_url = API_BASE_URL
25
+ # Subject context — set via set_subject() tool for personalization queries
26
+ self.subject_id: int | None = None
27
+
28
+ def _get_credentials(self) -> dict:
29
+ """Load credentials or raise descriptive error."""
30
+ creds = load_credentials()
31
+ if not creds or not creds.get("access_token"):
32
+ raise RuntimeError(
33
+ "Not authenticated. Please run 'nexotype-cli login' in your terminal first."
34
+ )
35
+ return creds
36
+
37
+ @property
38
+ def _headers(self) -> dict:
39
+ """Auth headers from stored credentials."""
40
+ creds = self._get_credentials()
41
+ return {"Authorization": f"Bearer {creds['access_token']}"}
42
+
43
+ async def _refresh_token(self) -> bool:
44
+ """Attempt to refresh JWT using refresh_token. Returns True if successful."""
45
+ creds = load_credentials()
46
+ if not creds or not creds.get("refresh_token"):
47
+ return False
48
+
49
+ try:
50
+ async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as http:
51
+ r = await http.post(
52
+ f"{self.base_url}/accounts/auth/refresh-token",
53
+ json={"refresh_token": creds["refresh_token"]},
54
+ )
55
+ if r.status_code == 200:
56
+ data = r.json()
57
+ creds["access_token"] = data["token"]["access_token"]
58
+ creds["refresh_token"] = data["token"]["refresh_token"]
59
+ save_credentials(creds)
60
+ return True
61
+ except httpx.HTTPError:
62
+ pass
63
+ return False
64
+
65
+ async def _request(self, method: str, path: str, **kwargs) -> dict:
66
+ """Make HTTP request with auto-refresh on 401."""
67
+ async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as http:
68
+ r = await http.request(
69
+ method,
70
+ f"{self.base_url}{path}",
71
+ headers=self._headers,
72
+ **kwargs,
73
+ )
74
+
75
+ # Auto-refresh on 401
76
+ if r.status_code == 401:
77
+ refreshed = await self._refresh_token()
78
+ if refreshed:
79
+ r = await http.request(
80
+ method,
81
+ f"{self.base_url}{path}",
82
+ headers=self._headers,
83
+ **kwargs,
84
+ )
85
+ else:
86
+ raise RuntimeError(
87
+ "Session expired. Please run 'nexotype-cli login' again."
88
+ )
89
+
90
+ r.raise_for_status()
91
+ return r.json()
92
+
93
+ async def get(self, path: str, **kwargs) -> dict:
94
+ """GET request to Nexotype API."""
95
+ return await self._request("GET", path, **kwargs)
96
+
97
+ async def post(self, path: str, data: dict) -> dict:
98
+ """POST request to Nexotype API."""
99
+ return await self._request("POST", path, json=data)
100
+
101
+ async def put(self, path: str, data: dict) -> dict:
102
+ """PUT request to Nexotype API."""
103
+ return await self._request("PUT", path, json=data)
104
+
105
+ async def post_file(self, path: str, file_path: str, form_data: dict) -> dict:
106
+ """
107
+ POST multipart file upload to Nexotype API.
108
+ Used for VCF/23andMe genomic file uploads.
109
+ Longer timeout (120s) for large files.
110
+ """
111
+ async with httpx.AsyncClient(timeout=UPLOAD_TIMEOUT) as http:
112
+ with open(file_path, "rb") as f:
113
+ files = {"file": (file_path.split("/")[-1], f)}
114
+ r = await http.post(
115
+ f"{self.base_url}{path}",
116
+ headers=self._headers,
117
+ files=files,
118
+ data=form_data,
119
+ )
120
+
121
+ # Auto-refresh on 401
122
+ if r.status_code == 401:
123
+ refreshed = await self._refresh_token()
124
+ if refreshed:
125
+ with open(file_path, "rb") as f:
126
+ files = {"file": (file_path.split("/")[-1], f)}
127
+ r = await http.post(
128
+ f"{self.base_url}{path}",
129
+ headers=self._headers,
130
+ files=files,
131
+ data=form_data,
132
+ )
133
+ else:
134
+ raise RuntimeError(
135
+ "Session expired. Please run 'nexotype-cli login' again."
136
+ )
137
+
138
+ r.raise_for_status()
139
+ return r.json()
140
+
141
+
142
+ # Singleton instance — shared across all tools
143
+ nexotype = NexotypeClient()
@@ -0,0 +1,20 @@
1
+ """
2
+ Nexotype MCP Server — Configuration
3
+
4
+ API base URL is read from NEXOTYPE_API_URL env var.
5
+ Defaults to localhost for development, override for production.
6
+ """
7
+
8
+ import os
9
+
10
+ # API base URL — override with NEXOTYPE_API_URL env var for production
11
+ API_BASE_URL = os.environ.get("NEXOTYPE_API_URL", "http://localhost:8000")
12
+
13
+ # Credentials file path — where JWT + refresh token are stored locally
14
+ CREDENTIALS_PATH = os.path.expanduser("~/.nexotype/credentials.json")
15
+
16
+ # HTTP client timeout (seconds) — standard requests
17
+ REQUEST_TIMEOUT = 30.0
18
+
19
+ # HTTP client timeout (seconds) — file uploads (VCF can be up to 500MB)
20
+ UPLOAD_TIMEOUT = 120.0
@@ -0,0 +1,50 @@
1
+ """
2
+ Nexotype MCP Server — Credentials Management
3
+
4
+ Reads/writes JWT + refresh token to ~/.nexotype/credentials.json.
5
+ File permissions set to 600 (owner read/write only).
6
+ Password is NEVER stored — only tokens.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+ from typing import TypedDict
13
+
14
+ from .config import CREDENTIALS_PATH
15
+
16
+
17
+ class Credentials(TypedDict):
18
+ access_token: str
19
+ refresh_token: str
20
+ organization_id: int | None
21
+ user_email: str
22
+
23
+
24
+ def load_credentials() -> Credentials | None:
25
+ """Load credentials from ~/.nexotype/credentials.json. Returns None if not found."""
26
+ path = Path(CREDENTIALS_PATH)
27
+ if not path.exists():
28
+ return None
29
+ try:
30
+ with open(path) as f:
31
+ return json.load(f)
32
+ except (json.JSONDecodeError, KeyError):
33
+ return None
34
+
35
+
36
+ def save_credentials(creds: Credentials) -> None:
37
+ """Save credentials to ~/.nexotype/credentials.json with chmod 600."""
38
+ path = Path(CREDENTIALS_PATH)
39
+ path.parent.mkdir(parents=True, exist_ok=True)
40
+ with open(path, "w") as f:
41
+ json.dump(creds, f, indent=2)
42
+ # Owner read/write only — no group/other access
43
+ os.chmod(path, 0o600)
44
+
45
+
46
+ def delete_credentials() -> None:
47
+ """Delete credentials file (logout)."""
48
+ path = Path(CREDENTIALS_PATH)
49
+ if path.exists():
50
+ path.unlink()