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.
- nexotype_mcp-0.1.0/PKG-INFO +176 -0
- nexotype_mcp-0.1.0/README.md +153 -0
- nexotype_mcp-0.1.0/pyproject.toml +37 -0
- nexotype_mcp-0.1.0/src/nexotype_mcp/__init__.py +1 -0
- nexotype_mcp-0.1.0/src/nexotype_mcp/cli.py +102 -0
- nexotype_mcp-0.1.0/src/nexotype_mcp/client.py +143 -0
- nexotype_mcp-0.1.0/src/nexotype_mcp/config.py +20 -0
- nexotype_mcp-0.1.0/src/nexotype_mcp/credentials.py +50 -0
- nexotype_mcp-0.1.0/src/nexotype_mcp/server.py +52 -0
- nexotype_mcp-0.1.0/src/nexotype_mcp/tools/__init__.py +0 -0
- nexotype_mcp-0.1.0/src/nexotype_mcp/tools/ai.py +59 -0
- nexotype_mcp-0.1.0/src/nexotype_mcp/tools/commercial.py +208 -0
- nexotype_mcp-0.1.0/src/nexotype_mcp/tools/context.py +98 -0
- nexotype_mcp-0.1.0/src/nexotype_mcp/tools/graph.py +419 -0
- nexotype_mcp-0.1.0/src/nexotype_mcp/tools/search.py +233 -0
- nexotype_mcp-0.1.0/src/nexotype_mcp/tools/user.py +329 -0
|
@@ -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()
|