getbased-mcp 0.2.4__tar.gz → 0.2.6__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.
- getbased_mcp-0.2.6/LICENSE +20 -0
- {getbased_mcp-0.2.4/getbased_mcp.egg-info → getbased_mcp-0.2.6}/PKG-INFO +22 -13
- getbased_mcp-0.2.4/PKG-INFO → getbased_mcp-0.2.6/README.md +19 -27
- getbased_mcp-0.2.4/README.md → getbased_mcp-0.2.6/getbased_mcp.egg-info/PKG-INFO +36 -11
- {getbased_mcp-0.2.4 → getbased_mcp-0.2.6}/getbased_mcp.egg-info/requires.txt +1 -0
- {getbased_mcp-0.2.4 → getbased_mcp-0.2.6}/getbased_mcp.py +142 -5
- {getbased_mcp-0.2.4 → getbased_mcp-0.2.6}/pyproject.toml +3 -2
- {getbased_mcp-0.2.4 → getbased_mcp-0.2.6}/tests/test_tools.py +143 -0
- getbased_mcp-0.2.4/LICENSE +0 -22
- {getbased_mcp-0.2.4 → getbased_mcp-0.2.6}/getbased_mcp.egg-info/SOURCES.txt +0 -0
- {getbased_mcp-0.2.4 → getbased_mcp-0.2.6}/getbased_mcp.egg-info/dependency_links.txt +0 -0
- {getbased_mcp-0.2.4 → getbased_mcp-0.2.6}/getbased_mcp.egg-info/entry_points.txt +0 -0
- {getbased_mcp-0.2.4 → getbased_mcp-0.2.6}/getbased_mcp.egg-info/top_level.txt +0 -0
- {getbased_mcp-0.2.4 → getbased_mcp-0.2.6}/setup.cfg +0 -0
- {getbased_mcp-0.2.4 → getbased_mcp-0.2.6}/tests/test_env_loader.py +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 3, 19 November 2007
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
5
|
+
Everyone is permitted to copy and distribute verbatim copies
|
|
6
|
+
of this license document, but changing it is not allowed.
|
|
7
|
+
|
|
8
|
+
Preamble
|
|
9
|
+
|
|
10
|
+
The GNU Affero General Public License is a free, copyleft license for
|
|
11
|
+
software and other kinds of works, specifically designed to ensure
|
|
12
|
+
cooperation with the community in the case of network server software.
|
|
13
|
+
|
|
14
|
+
The licenses for most software and other practical works are designed
|
|
15
|
+
to take away your freedom to share and change the works. By contrast,
|
|
16
|
+
our General Public Licenses are intended to guarantee your freedom to
|
|
17
|
+
share and change all versions of a program--to make sure it remains free
|
|
18
|
+
software for all its users.
|
|
19
|
+
|
|
20
|
+
For the full license text, see <https://www.gnu.org/licenses/agpl-3.0.txt>
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: getbased-mcp
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: MCP server for querying blood work data and knowledge base from getbased
|
|
5
|
-
License-Expression:
|
|
5
|
+
License-Expression: AGPL-3.0-or-later
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Requires-Dist: mcp>=1.0.0
|
|
10
10
|
Requires-Dist: httpx>=0.27
|
|
11
|
+
Requires-Dist: cryptography>=42.0
|
|
11
12
|
Provides-Extra: test
|
|
12
13
|
Requires-Dist: pytest>=8.0; extra == "test"
|
|
13
14
|
Requires-Dist: pytest-asyncio>=0.23; extra == "test"
|
|
@@ -25,11 +26,13 @@ An [MCP](https://modelcontextprotocol.io) server that exposes blood work data an
|
|
|
25
26
|
```
|
|
26
27
|
getbased (browser)
|
|
27
28
|
├── your data, your mnemonic
|
|
28
|
-
├── generates a read-only token
|
|
29
|
-
|
|
29
|
+
├── generates a read-only relay token
|
|
30
|
+
├── generates a separate Agent Context encryption key
|
|
31
|
+
├── encrypts the rendered agent context with that context key
|
|
32
|
+
└── pushes ciphertext to sync gateway on every save
|
|
30
33
|
|
|
31
34
|
Sync Gateway (sync.getbased.health/api/context)
|
|
32
|
-
└── stores context
|
|
35
|
+
└── stores encrypted context behind token auth; it cannot read the plaintext
|
|
33
36
|
|
|
34
37
|
RAG Server (localhost, optional)
|
|
35
38
|
├── Vector database with embedded chunks
|
|
@@ -42,7 +45,7 @@ This MCP Server (on your machine)
|
|
|
42
45
|
└── exposes everything as tools to any MCP client
|
|
43
46
|
```
|
|
44
47
|
|
|
45
|
-
Your mnemonic never leaves your browser. The
|
|
48
|
+
Your mnemonic never leaves your browser. The sync gateway receives only an end-to-end encrypted agent-context envelope; this MCP decrypts it locally with `GETBASED_AGENT_CONTEXT_KEY` and then exposes the same lab context text the getbased AI chat uses — not raw data. `GETBASED_TOKEN` is only the relay bearer token.
|
|
46
49
|
|
|
47
50
|
## Tools
|
|
48
51
|
|
|
@@ -116,9 +119,12 @@ The gateway stores context per profile ID. To work with multiple profiles:
|
|
|
116
119
|
|
|
117
120
|
## Setup
|
|
118
121
|
|
|
119
|
-
### 1. Enable
|
|
122
|
+
### 1. Enable Agent Access in getbased
|
|
120
123
|
|
|
121
|
-
Go to **Settings > Data >
|
|
124
|
+
Go to **Settings > Data > Agent Access** and toggle it on. Copy both values:
|
|
125
|
+
|
|
126
|
+
- **Read-only token** → `GETBASED_TOKEN` for relay authorization
|
|
127
|
+
- **Context encryption key** → `GETBASED_AGENT_CONTEXT_KEY` for local decryption
|
|
122
128
|
|
|
123
129
|
### 2. Set up a RAG server (optional — for knowledge_search)
|
|
124
130
|
|
|
@@ -152,7 +158,8 @@ Add to your MCP config (`~/.claude/claude_desktop_config.json` or similar):
|
|
|
152
158
|
"command": "python3",
|
|
153
159
|
"args": ["/path/to/getbased_mcp.py"],
|
|
154
160
|
"env": {
|
|
155
|
-
"GETBASED_TOKEN": "your-token-here"
|
|
161
|
+
"GETBASED_TOKEN": "your-token-here",
|
|
162
|
+
"GETBASED_AGENT_CONTEXT_KEY": "your-context-key-here"
|
|
156
163
|
}
|
|
157
164
|
}
|
|
158
165
|
}
|
|
@@ -167,7 +174,7 @@ hermes mcp add getbased \
|
|
|
167
174
|
--args /path/to/getbased_mcp.py
|
|
168
175
|
```
|
|
169
176
|
|
|
170
|
-
Then set `GETBASED_TOKEN` in `~/.hermes/.env` or in the MCP server's `env` config in `config.yaml`:
|
|
177
|
+
Then set `GETBASED_TOKEN` and `GETBASED_AGENT_CONTEXT_KEY` in `~/.hermes/.env` or in the MCP server's `env` config in `config.yaml`:
|
|
171
178
|
|
|
172
179
|
```yaml
|
|
173
180
|
mcp_servers:
|
|
@@ -176,6 +183,7 @@ mcp_servers:
|
|
|
176
183
|
args: [/path/to/getbased_mcp.py]
|
|
177
184
|
env:
|
|
178
185
|
GETBASED_TOKEN: your-token-here
|
|
186
|
+
GETBASED_AGENT_CONTEXT_KEY: your-context-key-here
|
|
179
187
|
```
|
|
180
188
|
|
|
181
189
|
### 4. Use it
|
|
@@ -191,7 +199,8 @@ Ask about your labs in any connected conversation:
|
|
|
191
199
|
|
|
192
200
|
| Variable | Required | Description |
|
|
193
201
|
|---|---|---|
|
|
194
|
-
| `GETBASED_TOKEN` | Yes | Read-only token from getbased Settings > Data >
|
|
202
|
+
| `GETBASED_TOKEN` | Yes | Read-only bearer token from getbased Settings > Data > Agent Access; authorizes fetches from the context gateway |
|
|
203
|
+
| `GETBASED_AGENT_CONTEXT_KEY` | Yes for encrypted Agent Access payloads | Context encryption key from getbased Settings > Data > Agent Access; decrypts the relay payload locally in this MCP |
|
|
195
204
|
| `GETBASED_GATEWAY` | No | Context gateway URL (default: `https://sync.getbased.health`) |
|
|
196
205
|
| `LENS_URL` | No | RAG server URL (default: `http://localhost:8322`). Overrides `LENS_PORT` |
|
|
197
206
|
| `LENS_PORT` | No | RAG server port, only used to build default `LENS_URL` (default: `8322`) |
|
|
@@ -241,8 +250,8 @@ That's expected — they're independent. Blood work tools talk to the sync gatew
|
|
|
241
250
|
|
|
242
251
|
## Related projects
|
|
243
252
|
|
|
244
|
-
- **[getbased](https://github.com/elkimek/get-based)** — the health dashboard. This MCP reads the same lab context the in-app AI chat uses, and queries the same Knowledge Source endpoint configured in Settings → AI → Custom Knowledge Source. The [endpoint contract](https://github.com/elkimek/get-based/blob/main/docs/
|
|
253
|
+
- **[getbased](https://github.com/elkimek/get-based)** — the health dashboard. This MCP reads the same lab context the in-app AI chat uses, and queries the same Knowledge Source endpoint configured in Settings → AI → Custom Knowledge Source. The [endpoint contract](https://github.com/elkimek/get-based/blob/main/dev-docs/lens-endpoint-contract.md) is shared — one server backs both the app and this MCP.
|
|
245
254
|
|
|
246
255
|
## License
|
|
247
256
|
|
|
248
|
-
|
|
257
|
+
AGPL-3.0-or-later
|
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: getbased-mcp
|
|
3
|
-
Version: 0.2.4
|
|
4
|
-
Summary: MCP server for querying blood work data and knowledge base from getbased
|
|
5
|
-
License-Expression: GPL-3.0-only
|
|
6
|
-
Requires-Python: >=3.10
|
|
7
|
-
Description-Content-Type: text/markdown
|
|
8
|
-
License-File: LICENSE
|
|
9
|
-
Requires-Dist: mcp>=1.0.0
|
|
10
|
-
Requires-Dist: httpx>=0.27
|
|
11
|
-
Provides-Extra: test
|
|
12
|
-
Requires-Dist: pytest>=8.0; extra == "test"
|
|
13
|
-
Requires-Dist: pytest-asyncio>=0.23; extra == "test"
|
|
14
|
-
Requires-Dist: respx>=0.21; extra == "test"
|
|
15
|
-
Dynamic: license-file
|
|
16
|
-
|
|
17
1
|
# getbased MCP Server
|
|
18
2
|
|
|
19
3
|
An [MCP](https://modelcontextprotocol.io) server that exposes blood work data and an optional RAG knowledge base from [getbased](https://getbased.health) as tools. Works with any MCP-compatible client (Claude Code, Hermes, Claude Desktop, etc.).
|
|
@@ -25,11 +9,13 @@ An [MCP](https://modelcontextprotocol.io) server that exposes blood work data an
|
|
|
25
9
|
```
|
|
26
10
|
getbased (browser)
|
|
27
11
|
├── your data, your mnemonic
|
|
28
|
-
├── generates a read-only token
|
|
29
|
-
|
|
12
|
+
├── generates a read-only relay token
|
|
13
|
+
├── generates a separate Agent Context encryption key
|
|
14
|
+
├── encrypts the rendered agent context with that context key
|
|
15
|
+
└── pushes ciphertext to sync gateway on every save
|
|
30
16
|
|
|
31
17
|
Sync Gateway (sync.getbased.health/api/context)
|
|
32
|
-
└── stores context
|
|
18
|
+
└── stores encrypted context behind token auth; it cannot read the plaintext
|
|
33
19
|
|
|
34
20
|
RAG Server (localhost, optional)
|
|
35
21
|
├── Vector database with embedded chunks
|
|
@@ -42,7 +28,7 @@ This MCP Server (on your machine)
|
|
|
42
28
|
└── exposes everything as tools to any MCP client
|
|
43
29
|
```
|
|
44
30
|
|
|
45
|
-
Your mnemonic never leaves your browser. The
|
|
31
|
+
Your mnemonic never leaves your browser. The sync gateway receives only an end-to-end encrypted agent-context envelope; this MCP decrypts it locally with `GETBASED_AGENT_CONTEXT_KEY` and then exposes the same lab context text the getbased AI chat uses — not raw data. `GETBASED_TOKEN` is only the relay bearer token.
|
|
46
32
|
|
|
47
33
|
## Tools
|
|
48
34
|
|
|
@@ -116,9 +102,12 @@ The gateway stores context per profile ID. To work with multiple profiles:
|
|
|
116
102
|
|
|
117
103
|
## Setup
|
|
118
104
|
|
|
119
|
-
### 1. Enable
|
|
105
|
+
### 1. Enable Agent Access in getbased
|
|
106
|
+
|
|
107
|
+
Go to **Settings > Data > Agent Access** and toggle it on. Copy both values:
|
|
120
108
|
|
|
121
|
-
|
|
109
|
+
- **Read-only token** → `GETBASED_TOKEN` for relay authorization
|
|
110
|
+
- **Context encryption key** → `GETBASED_AGENT_CONTEXT_KEY` for local decryption
|
|
122
111
|
|
|
123
112
|
### 2. Set up a RAG server (optional — for knowledge_search)
|
|
124
113
|
|
|
@@ -152,7 +141,8 @@ Add to your MCP config (`~/.claude/claude_desktop_config.json` or similar):
|
|
|
152
141
|
"command": "python3",
|
|
153
142
|
"args": ["/path/to/getbased_mcp.py"],
|
|
154
143
|
"env": {
|
|
155
|
-
"GETBASED_TOKEN": "your-token-here"
|
|
144
|
+
"GETBASED_TOKEN": "your-token-here",
|
|
145
|
+
"GETBASED_AGENT_CONTEXT_KEY": "your-context-key-here"
|
|
156
146
|
}
|
|
157
147
|
}
|
|
158
148
|
}
|
|
@@ -167,7 +157,7 @@ hermes mcp add getbased \
|
|
|
167
157
|
--args /path/to/getbased_mcp.py
|
|
168
158
|
```
|
|
169
159
|
|
|
170
|
-
Then set `GETBASED_TOKEN` in `~/.hermes/.env` or in the MCP server's `env` config in `config.yaml`:
|
|
160
|
+
Then set `GETBASED_TOKEN` and `GETBASED_AGENT_CONTEXT_KEY` in `~/.hermes/.env` or in the MCP server's `env` config in `config.yaml`:
|
|
171
161
|
|
|
172
162
|
```yaml
|
|
173
163
|
mcp_servers:
|
|
@@ -176,6 +166,7 @@ mcp_servers:
|
|
|
176
166
|
args: [/path/to/getbased_mcp.py]
|
|
177
167
|
env:
|
|
178
168
|
GETBASED_TOKEN: your-token-here
|
|
169
|
+
GETBASED_AGENT_CONTEXT_KEY: your-context-key-here
|
|
179
170
|
```
|
|
180
171
|
|
|
181
172
|
### 4. Use it
|
|
@@ -191,7 +182,8 @@ Ask about your labs in any connected conversation:
|
|
|
191
182
|
|
|
192
183
|
| Variable | Required | Description |
|
|
193
184
|
|---|---|---|
|
|
194
|
-
| `GETBASED_TOKEN` | Yes | Read-only token from getbased Settings > Data >
|
|
185
|
+
| `GETBASED_TOKEN` | Yes | Read-only bearer token from getbased Settings > Data > Agent Access; authorizes fetches from the context gateway |
|
|
186
|
+
| `GETBASED_AGENT_CONTEXT_KEY` | Yes for encrypted Agent Access payloads | Context encryption key from getbased Settings > Data > Agent Access; decrypts the relay payload locally in this MCP |
|
|
195
187
|
| `GETBASED_GATEWAY` | No | Context gateway URL (default: `https://sync.getbased.health`) |
|
|
196
188
|
| `LENS_URL` | No | RAG server URL (default: `http://localhost:8322`). Overrides `LENS_PORT` |
|
|
197
189
|
| `LENS_PORT` | No | RAG server port, only used to build default `LENS_URL` (default: `8322`) |
|
|
@@ -241,8 +233,8 @@ That's expected — they're independent. Blood work tools talk to the sync gatew
|
|
|
241
233
|
|
|
242
234
|
## Related projects
|
|
243
235
|
|
|
244
|
-
- **[getbased](https://github.com/elkimek/get-based)** — the health dashboard. This MCP reads the same lab context the in-app AI chat uses, and queries the same Knowledge Source endpoint configured in Settings → AI → Custom Knowledge Source. The [endpoint contract](https://github.com/elkimek/get-based/blob/main/docs/
|
|
236
|
+
- **[getbased](https://github.com/elkimek/get-based)** — the health dashboard. This MCP reads the same lab context the in-app AI chat uses, and queries the same Knowledge Source endpoint configured in Settings → AI → Custom Knowledge Source. The [endpoint contract](https://github.com/elkimek/get-based/blob/main/dev-docs/lens-endpoint-contract.md) is shared — one server backs both the app and this MCP.
|
|
245
237
|
|
|
246
238
|
## License
|
|
247
239
|
|
|
248
|
-
|
|
240
|
+
AGPL-3.0-or-later
|
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: getbased-mcp
|
|
3
|
+
Version: 0.2.6
|
|
4
|
+
Summary: MCP server for querying blood work data and knowledge base from getbased
|
|
5
|
+
License-Expression: AGPL-3.0-or-later
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: mcp>=1.0.0
|
|
10
|
+
Requires-Dist: httpx>=0.27
|
|
11
|
+
Requires-Dist: cryptography>=42.0
|
|
12
|
+
Provides-Extra: test
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "test"
|
|
15
|
+
Requires-Dist: respx>=0.21; extra == "test"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
1
18
|
# getbased MCP Server
|
|
2
19
|
|
|
3
20
|
An [MCP](https://modelcontextprotocol.io) server that exposes blood work data and an optional RAG knowledge base from [getbased](https://getbased.health) as tools. Works with any MCP-compatible client (Claude Code, Hermes, Claude Desktop, etc.).
|
|
@@ -9,11 +26,13 @@ An [MCP](https://modelcontextprotocol.io) server that exposes blood work data an
|
|
|
9
26
|
```
|
|
10
27
|
getbased (browser)
|
|
11
28
|
├── your data, your mnemonic
|
|
12
|
-
├── generates a read-only token
|
|
13
|
-
|
|
29
|
+
├── generates a read-only relay token
|
|
30
|
+
├── generates a separate Agent Context encryption key
|
|
31
|
+
├── encrypts the rendered agent context with that context key
|
|
32
|
+
└── pushes ciphertext to sync gateway on every save
|
|
14
33
|
|
|
15
34
|
Sync Gateway (sync.getbased.health/api/context)
|
|
16
|
-
└── stores context
|
|
35
|
+
└── stores encrypted context behind token auth; it cannot read the plaintext
|
|
17
36
|
|
|
18
37
|
RAG Server (localhost, optional)
|
|
19
38
|
├── Vector database with embedded chunks
|
|
@@ -26,7 +45,7 @@ This MCP Server (on your machine)
|
|
|
26
45
|
└── exposes everything as tools to any MCP client
|
|
27
46
|
```
|
|
28
47
|
|
|
29
|
-
Your mnemonic never leaves your browser. The
|
|
48
|
+
Your mnemonic never leaves your browser. The sync gateway receives only an end-to-end encrypted agent-context envelope; this MCP decrypts it locally with `GETBASED_AGENT_CONTEXT_KEY` and then exposes the same lab context text the getbased AI chat uses — not raw data. `GETBASED_TOKEN` is only the relay bearer token.
|
|
30
49
|
|
|
31
50
|
## Tools
|
|
32
51
|
|
|
@@ -100,9 +119,12 @@ The gateway stores context per profile ID. To work with multiple profiles:
|
|
|
100
119
|
|
|
101
120
|
## Setup
|
|
102
121
|
|
|
103
|
-
### 1. Enable
|
|
122
|
+
### 1. Enable Agent Access in getbased
|
|
123
|
+
|
|
124
|
+
Go to **Settings > Data > Agent Access** and toggle it on. Copy both values:
|
|
104
125
|
|
|
105
|
-
|
|
126
|
+
- **Read-only token** → `GETBASED_TOKEN` for relay authorization
|
|
127
|
+
- **Context encryption key** → `GETBASED_AGENT_CONTEXT_KEY` for local decryption
|
|
106
128
|
|
|
107
129
|
### 2. Set up a RAG server (optional — for knowledge_search)
|
|
108
130
|
|
|
@@ -136,7 +158,8 @@ Add to your MCP config (`~/.claude/claude_desktop_config.json` or similar):
|
|
|
136
158
|
"command": "python3",
|
|
137
159
|
"args": ["/path/to/getbased_mcp.py"],
|
|
138
160
|
"env": {
|
|
139
|
-
"GETBASED_TOKEN": "your-token-here"
|
|
161
|
+
"GETBASED_TOKEN": "your-token-here",
|
|
162
|
+
"GETBASED_AGENT_CONTEXT_KEY": "your-context-key-here"
|
|
140
163
|
}
|
|
141
164
|
}
|
|
142
165
|
}
|
|
@@ -151,7 +174,7 @@ hermes mcp add getbased \
|
|
|
151
174
|
--args /path/to/getbased_mcp.py
|
|
152
175
|
```
|
|
153
176
|
|
|
154
|
-
Then set `GETBASED_TOKEN` in `~/.hermes/.env` or in the MCP server's `env` config in `config.yaml`:
|
|
177
|
+
Then set `GETBASED_TOKEN` and `GETBASED_AGENT_CONTEXT_KEY` in `~/.hermes/.env` or in the MCP server's `env` config in `config.yaml`:
|
|
155
178
|
|
|
156
179
|
```yaml
|
|
157
180
|
mcp_servers:
|
|
@@ -160,6 +183,7 @@ mcp_servers:
|
|
|
160
183
|
args: [/path/to/getbased_mcp.py]
|
|
161
184
|
env:
|
|
162
185
|
GETBASED_TOKEN: your-token-here
|
|
186
|
+
GETBASED_AGENT_CONTEXT_KEY: your-context-key-here
|
|
163
187
|
```
|
|
164
188
|
|
|
165
189
|
### 4. Use it
|
|
@@ -175,7 +199,8 @@ Ask about your labs in any connected conversation:
|
|
|
175
199
|
|
|
176
200
|
| Variable | Required | Description |
|
|
177
201
|
|---|---|---|
|
|
178
|
-
| `GETBASED_TOKEN` | Yes | Read-only token from getbased Settings > Data >
|
|
202
|
+
| `GETBASED_TOKEN` | Yes | Read-only bearer token from getbased Settings > Data > Agent Access; authorizes fetches from the context gateway |
|
|
203
|
+
| `GETBASED_AGENT_CONTEXT_KEY` | Yes for encrypted Agent Access payloads | Context encryption key from getbased Settings > Data > Agent Access; decrypts the relay payload locally in this MCP |
|
|
179
204
|
| `GETBASED_GATEWAY` | No | Context gateway URL (default: `https://sync.getbased.health`) |
|
|
180
205
|
| `LENS_URL` | No | RAG server URL (default: `http://localhost:8322`). Overrides `LENS_PORT` |
|
|
181
206
|
| `LENS_PORT` | No | RAG server port, only used to build default `LENS_URL` (default: `8322`) |
|
|
@@ -225,8 +250,8 @@ That's expected — they're independent. Blood work tools talk to the sync gatew
|
|
|
225
250
|
|
|
226
251
|
## Related projects
|
|
227
252
|
|
|
228
|
-
- **[getbased](https://github.com/elkimek/get-based)** — the health dashboard. This MCP reads the same lab context the in-app AI chat uses, and queries the same Knowledge Source endpoint configured in Settings → AI → Custom Knowledge Source. The [endpoint contract](https://github.com/elkimek/get-based/blob/main/docs/
|
|
253
|
+
- **[getbased](https://github.com/elkimek/get-based)** — the health dashboard. This MCP reads the same lab context the in-app AI chat uses, and queries the same Knowledge Source endpoint configured in Settings → AI → Custom Knowledge Source. The [endpoint contract](https://github.com/elkimek/get-based/blob/main/dev-docs/lens-endpoint-contract.md) is shared — one server backs both the app and this MCP.
|
|
229
254
|
|
|
230
255
|
## License
|
|
231
256
|
|
|
232
|
-
|
|
257
|
+
AGPL-3.0-or-later
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""getbased MCP server — exposes blood work data and knowledge base search as tools.
|
|
3
3
|
|
|
4
4
|
Architecture:
|
|
5
|
-
getbased (browser) → sync gateway → this MCP →
|
|
5
|
+
getbased (browser encrypts context) → sync gateway → this MCP decrypts → AI client
|
|
6
6
|
↕
|
|
7
7
|
Lens RAG server (Qdrant + BGE-M3)
|
|
8
8
|
|
|
@@ -11,7 +11,10 @@ Knowledge base queries go through the Lens RAG server (separate process).
|
|
|
11
11
|
No models are loaded in this process — everything is HTTP.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
+
import base64
|
|
15
|
+
import binascii
|
|
14
16
|
import functools
|
|
17
|
+
import hashlib
|
|
15
18
|
import json
|
|
16
19
|
import logging
|
|
17
20
|
import os
|
|
@@ -19,6 +22,10 @@ import re
|
|
|
19
22
|
import time
|
|
20
23
|
|
|
21
24
|
import httpx
|
|
25
|
+
from cryptography.exceptions import InvalidTag
|
|
26
|
+
from cryptography.hazmat.primitives import hashes
|
|
27
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
28
|
+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
22
29
|
from mcp.server.fastmcp import FastMCP
|
|
23
30
|
|
|
24
31
|
|
|
@@ -63,7 +70,10 @@ mcp = FastMCP("getbased")
|
|
|
63
70
|
|
|
64
71
|
# ── Config ───────────────────────────────────────────────────────────
|
|
65
72
|
TOKEN = os.environ.get("GETBASED_TOKEN", "")
|
|
73
|
+
AGENT_CONTEXT_KEY = os.environ.get("GETBASED_AGENT_CONTEXT_KEY", "")
|
|
66
74
|
GATEWAY = os.environ.get("GETBASED_GATEWAY", "https://sync.getbased.health")
|
|
75
|
+
AGENT_CONTEXT_V1_KDF_INFO = b"getbased-agent-access-context-v1"
|
|
76
|
+
AGENT_CONTEXT_AAD_PREFIX = b"getbased-agent-context-v2"
|
|
67
77
|
|
|
68
78
|
LENS_URL = os.environ.get("LENS_URL", f"http://localhost:{os.environ.get('LENS_PORT', '8322')}")
|
|
69
79
|
|
|
@@ -178,8 +188,134 @@ def _instrumented(label: str):
|
|
|
178
188
|
|
|
179
189
|
# ── Helpers ──────────────────────────────────────────────────────────
|
|
180
190
|
|
|
181
|
-
|
|
182
|
-
|
|
191
|
+
def _token_bytes(token: str) -> bytes:
|
|
192
|
+
token = (token or "").strip()
|
|
193
|
+
if re.fullmatch(r"[0-9a-fA-F]{64}", token):
|
|
194
|
+
try:
|
|
195
|
+
return bytes.fromhex(token)
|
|
196
|
+
except ValueError:
|
|
197
|
+
pass
|
|
198
|
+
return token.encode("utf-8")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _decode_agent_context_key(value: str) -> bytes:
|
|
202
|
+
key = (value or "").strip()
|
|
203
|
+
if not key:
|
|
204
|
+
raise ValueError("GETBASED_AGENT_CONTEXT_KEY not set")
|
|
205
|
+
if key.startswith("gbctx_v1_"):
|
|
206
|
+
key = key[len("gbctx_v1_"):]
|
|
207
|
+
padded = key + "=" * (-len(key) % 4)
|
|
208
|
+
try:
|
|
209
|
+
raw = base64.urlsafe_b64decode(padded.encode("ascii"))
|
|
210
|
+
except (binascii.Error, UnicodeEncodeError) as e:
|
|
211
|
+
raise ValueError("GETBASED_AGENT_CONTEXT_KEY is not valid base64url") from e
|
|
212
|
+
if len(raw) != 32:
|
|
213
|
+
raise ValueError("GETBASED_AGENT_CONTEXT_KEY must decode to 32 bytes")
|
|
214
|
+
return raw
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _agent_context_key_id(raw_key: bytes) -> str:
|
|
218
|
+
return base64.urlsafe_b64encode(hashlib.sha256(raw_key).digest()[:12]).decode("ascii").rstrip("=")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _b64decode_required(value: object, field: str) -> bytes:
|
|
222
|
+
if not isinstance(value, str) or not value:
|
|
223
|
+
raise ValueError(f"encrypted context missing {field}")
|
|
224
|
+
try:
|
|
225
|
+
return base64.b64decode(value, validate=True)
|
|
226
|
+
except binascii.Error as e:
|
|
227
|
+
raise ValueError(f"encrypted context has invalid base64 in {field}") from e
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _decrypt_agent_context(envelope: dict, profile_id: str) -> str:
|
|
231
|
+
"""Decrypt a browser-produced Agent Access context envelope.
|
|
232
|
+
|
|
233
|
+
v2 separates relay authorization from content secrecy: GETBASED_TOKEN
|
|
234
|
+
authorizes the HTTPS fetch, while GETBASED_AGENT_CONTEXT_KEY decrypts the
|
|
235
|
+
AES-256-GCM payload locally. v1 token-derived envelopes remain readable as
|
|
236
|
+
a temporary rollout fallback.
|
|
237
|
+
"""
|
|
238
|
+
if not isinstance(envelope, dict):
|
|
239
|
+
raise ValueError("encrypted context envelope is invalid")
|
|
240
|
+
version = envelope.get("version")
|
|
241
|
+
if envelope.get("alg") != "AES-256-GCM":
|
|
242
|
+
raise ValueError("unsupported encrypted context crypto parameters")
|
|
243
|
+
|
|
244
|
+
iv = _b64decode_required(envelope.get("iv"), "iv")
|
|
245
|
+
ciphertext = _b64decode_required(envelope.get("ciphertext"), "ciphertext")
|
|
246
|
+
|
|
247
|
+
if version == 2:
|
|
248
|
+
if envelope.get("keyDerivation") != "raw-256-bit-key":
|
|
249
|
+
raise ValueError("unsupported encrypted context crypto parameters")
|
|
250
|
+
raw_key = _decode_agent_context_key(AGENT_CONTEXT_KEY)
|
|
251
|
+
key_id = envelope.get("keyId")
|
|
252
|
+
if key_id and key_id != _agent_context_key_id(raw_key):
|
|
253
|
+
raise ValueError("encrypted context could not be decrypted with this Agent Context key")
|
|
254
|
+
aad = AGENT_CONTEXT_AAD_PREFIX + b":" + (profile_id or "default").encode("utf-8")
|
|
255
|
+
failure = "encrypted context could not be decrypted with this Agent Context key"
|
|
256
|
+
elif version == 1:
|
|
257
|
+
if not TOKEN:
|
|
258
|
+
raise ValueError("GETBASED_TOKEN not set")
|
|
259
|
+
if envelope.get("kdf") != "HKDF-SHA-256":
|
|
260
|
+
raise ValueError("unsupported encrypted context crypto parameters")
|
|
261
|
+
salt = _b64decode_required(envelope.get("salt"), "salt")
|
|
262
|
+
raw_key = HKDF(
|
|
263
|
+
algorithm=hashes.SHA256(),
|
|
264
|
+
length=32,
|
|
265
|
+
salt=salt,
|
|
266
|
+
info=AGENT_CONTEXT_V1_KDF_INFO,
|
|
267
|
+
).derive(_token_bytes(TOKEN))
|
|
268
|
+
aad = AGENT_CONTEXT_V1_KDF_INFO + b":" + (profile_id or "default").encode("utf-8")
|
|
269
|
+
failure = "encrypted context could not be decrypted with this Agent Access token"
|
|
270
|
+
else:
|
|
271
|
+
raise ValueError("unsupported encrypted context version")
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
plaintext = AESGCM(raw_key).decrypt(iv, ciphertext, aad)
|
|
275
|
+
return plaintext.decode("utf-8")
|
|
276
|
+
except InvalidTag as e:
|
|
277
|
+
raise ValueError(failure) from e
|
|
278
|
+
except UnicodeDecodeError as e:
|
|
279
|
+
raise ValueError("encrypted context decrypted to invalid UTF-8") from e
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _decode_context_payload(data: dict) -> dict:
|
|
283
|
+
"""Return gateway payload with plaintext `context` restored for tool code.
|
|
284
|
+
|
|
285
|
+
Legacy plaintext gateway rows are still accepted so existing users do not
|
|
286
|
+
lose access during rollout. New browser builds publish an encrypted JSON
|
|
287
|
+
envelope inside the relay's legacy `context` string field, because the
|
|
288
|
+
deployed relay validates that `context` is a string.
|
|
289
|
+
"""
|
|
290
|
+
if not isinstance(data, dict):
|
|
291
|
+
return {"error": "getbased gateway returned invalid payload"}
|
|
292
|
+
envelope = data.get("encryptedContext")
|
|
293
|
+
context_value = data.get("context")
|
|
294
|
+
if not envelope and isinstance(context_value, str):
|
|
295
|
+
try:
|
|
296
|
+
parsed_context = json.loads(context_value)
|
|
297
|
+
if isinstance(parsed_context, dict):
|
|
298
|
+
envelope = parsed_context.get("encryptedContext")
|
|
299
|
+
except json.JSONDecodeError:
|
|
300
|
+
envelope = None
|
|
301
|
+
if envelope:
|
|
302
|
+
try:
|
|
303
|
+
profile_id = str(data.get("profileId") or "default")
|
|
304
|
+
decoded = dict(data)
|
|
305
|
+
decoded["context"] = _decrypt_agent_context(envelope, profile_id)
|
|
306
|
+
decoded.pop("encryptedContext", None)
|
|
307
|
+
return decoded
|
|
308
|
+
except ValueError as e:
|
|
309
|
+
return {"error": str(e)}
|
|
310
|
+
return data
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
async def _fetch_context(profile: str = "", *, decrypt_context: bool = True) -> dict:
|
|
314
|
+
"""Fetch formatted lab context from the getbased sync gateway.
|
|
315
|
+
|
|
316
|
+
Profile discovery only needs token-authenticated metadata. Keep it usable
|
|
317
|
+
even on token-only installs by letting callers opt out of context decrypt.
|
|
318
|
+
"""
|
|
183
319
|
if not TOKEN:
|
|
184
320
|
return {"error": "GETBASED_TOKEN not set"}
|
|
185
321
|
try:
|
|
@@ -191,7 +327,8 @@ async def _fetch_context(profile: str = "") -> dict:
|
|
|
191
327
|
params=params,
|
|
192
328
|
)
|
|
193
329
|
r.raise_for_status()
|
|
194
|
-
|
|
330
|
+
data = r.json()
|
|
331
|
+
return _decode_context_payload(data) if decrypt_context else data
|
|
195
332
|
except httpx.HTTPStatusError as e:
|
|
196
333
|
return {"error": f"getbased gateway returned {e.response.status_code}"}
|
|
197
334
|
except httpx.RequestError as e:
|
|
@@ -526,7 +663,7 @@ async def getbased_wearables_series(
|
|
|
526
663
|
@_instrumented("getbased_list_profiles")
|
|
527
664
|
async def getbased_list_profiles() -> str:
|
|
528
665
|
"""List all available profiles in getbased."""
|
|
529
|
-
data = await _fetch_context()
|
|
666
|
+
data = await _fetch_context(decrypt_context=False)
|
|
530
667
|
if "error" in data:
|
|
531
668
|
return f"Error: {data['error']}"
|
|
532
669
|
profiles = data.get("profiles") or []
|
|
@@ -4,14 +4,15 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "getbased-mcp"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.6"
|
|
8
8
|
description = "MCP server for querying blood work data and knowledge base from getbased"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
license = "
|
|
10
|
+
license = "AGPL-3.0-or-later"
|
|
11
11
|
requires-python = ">=3.10"
|
|
12
12
|
dependencies = [
|
|
13
13
|
"mcp>=1.0.0",
|
|
14
14
|
"httpx>=0.27",
|
|
15
|
+
"cryptography>=42.0",
|
|
15
16
|
]
|
|
16
17
|
|
|
17
18
|
[project.optional-dependencies]
|
|
@@ -8,8 +8,13 @@ Uses respx to intercept httpx calls. Verifies:
|
|
|
8
8
|
"""
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import json
|
|
12
|
+
|
|
11
13
|
import pytest
|
|
12
14
|
import respx
|
|
15
|
+
from cryptography.hazmat.primitives import hashes
|
|
16
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
17
|
+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
13
18
|
from httpx import Response
|
|
14
19
|
|
|
15
20
|
|
|
@@ -21,6 +26,47 @@ GATEWAY_CONTEXT_URL = "https://gateway.test/api/context"
|
|
|
21
26
|
LENS_URL_PREFIX = "http://lens.test:8322"
|
|
22
27
|
|
|
23
28
|
|
|
29
|
+
def _encrypted_context_payload(gm, context: str, profile_id: str = "abc", *, version: int = 2) -> dict:
|
|
30
|
+
import base64
|
|
31
|
+
|
|
32
|
+
iv = bytes(range(16, 28))
|
|
33
|
+
if version == 2:
|
|
34
|
+
raw_key = gm._decode_agent_context_key(gm.AGENT_CONTEXT_KEY)
|
|
35
|
+
aad = gm.AGENT_CONTEXT_AAD_PREFIX + b":" + profile_id.encode("utf-8")
|
|
36
|
+
ciphertext = AESGCM(raw_key).encrypt(iv, context.encode("utf-8"), aad)
|
|
37
|
+
envelope = {
|
|
38
|
+
"version": 2,
|
|
39
|
+
"alg": "AES-256-GCM",
|
|
40
|
+
"keyDerivation": "raw-256-bit-key",
|
|
41
|
+
"keyId": gm._agent_context_key_id(raw_key),
|
|
42
|
+
"iv": base64.b64encode(iv).decode("ascii"),
|
|
43
|
+
"ciphertext": base64.b64encode(ciphertext).decode("ascii"),
|
|
44
|
+
}
|
|
45
|
+
else:
|
|
46
|
+
salt = bytes(range(16))
|
|
47
|
+
key = HKDF(
|
|
48
|
+
algorithm=hashes.SHA256(),
|
|
49
|
+
length=32,
|
|
50
|
+
salt=salt,
|
|
51
|
+
info=gm.AGENT_CONTEXT_V1_KDF_INFO,
|
|
52
|
+
).derive(gm._token_bytes(gm.TOKEN))
|
|
53
|
+
aad = gm.AGENT_CONTEXT_V1_KDF_INFO + b":" + profile_id.encode("utf-8")
|
|
54
|
+
ciphertext = AESGCM(key).encrypt(iv, context.encode("utf-8"), aad)
|
|
55
|
+
envelope = {
|
|
56
|
+
"version": 1,
|
|
57
|
+
"alg": "AES-256-GCM",
|
|
58
|
+
"kdf": "HKDF-SHA-256",
|
|
59
|
+
"info": gm.AGENT_CONTEXT_V1_KDF_INFO.decode("utf-8"),
|
|
60
|
+
"salt": base64.b64encode(salt).decode("ascii"),
|
|
61
|
+
"iv": base64.b64encode(iv).decode("ascii"),
|
|
62
|
+
"ciphertext": base64.b64encode(ciphertext).decode("ascii"),
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
"profileId": profile_id,
|
|
66
|
+
"context": json.dumps({"encryptedContext": envelope}),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
24
70
|
@pytest.mark.asyncio
|
|
25
71
|
@respx.mock
|
|
26
72
|
async def test_getbased_list_profiles_happy(gm) -> None:
|
|
@@ -47,6 +93,20 @@ async def test_getbased_list_profiles_no_token(gm, monkeypatch: pytest.MonkeyPat
|
|
|
47
93
|
assert "GETBASED_TOKEN not set" in out
|
|
48
94
|
|
|
49
95
|
|
|
96
|
+
@pytest.mark.asyncio
|
|
97
|
+
@respx.mock
|
|
98
|
+
async def test_getbased_list_profiles_works_without_context_key_for_encrypted_payload(gm, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
99
|
+
payload = _encrypted_context_payload(gm, "[section:hormones]\nsecret\n[/section:hormones]", profile_id="abc")
|
|
100
|
+
payload["profiles"] = [{"id": "abc", "name": "Main"}]
|
|
101
|
+
monkeypatch.setattr(gm, "AGENT_CONTEXT_KEY", "")
|
|
102
|
+
respx.get(GATEWAY_CONTEXT_URL).mock(return_value=Response(200, json=payload))
|
|
103
|
+
|
|
104
|
+
out = await gm.getbased_list_profiles()
|
|
105
|
+
|
|
106
|
+
assert "abc Main" in out
|
|
107
|
+
assert "GETBASED_AGENT_CONTEXT_KEY" not in out
|
|
108
|
+
|
|
109
|
+
|
|
50
110
|
@pytest.mark.asyncio
|
|
51
111
|
@respx.mock
|
|
52
112
|
async def test_getbased_lab_context_happy(gm) -> None:
|
|
@@ -61,6 +121,89 @@ async def test_getbased_lab_context_happy(gm) -> None:
|
|
|
61
121
|
assert "testosterone" in out
|
|
62
122
|
|
|
63
123
|
|
|
124
|
+
@pytest.mark.asyncio
|
|
125
|
+
@respx.mock
|
|
126
|
+
async def test_getbased_lab_context_decrypts_agent_access_payload(gm) -> None:
|
|
127
|
+
plaintext = "[section:hormones]\ntestosterone: 18.4 nmol/L\n[/section:hormones]"
|
|
128
|
+
payload = _encrypted_context_payload(gm, plaintext, profile_id="abc")
|
|
129
|
+
assert "testosterone" not in payload["context"]
|
|
130
|
+
respx.get(GATEWAY_CONTEXT_URL).mock(return_value=Response(200, json=payload))
|
|
131
|
+
|
|
132
|
+
out = await gm.getbased_lab_context()
|
|
133
|
+
|
|
134
|
+
assert "Profile: abc" in out
|
|
135
|
+
assert "testosterone: 18.4" in out
|
|
136
|
+
assert "encryptedContext" not in out
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@pytest.mark.asyncio
|
|
140
|
+
@respx.mock
|
|
141
|
+
async def test_getbased_lab_context_rejects_wrong_agent_context_key(gm, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
142
|
+
payload = _encrypted_context_payload(gm, "[section:hormones]\nsecret\n[/section:hormones]", profile_id="abc")
|
|
143
|
+
monkeypatch.setattr(gm, "AGENT_CONTEXT_KEY", "gbctx_v1_ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8")
|
|
144
|
+
respx.get(GATEWAY_CONTEXT_URL).mock(return_value=Response(200, json=payload))
|
|
145
|
+
|
|
146
|
+
out = await gm.getbased_lab_context()
|
|
147
|
+
|
|
148
|
+
assert "Error:" in out
|
|
149
|
+
assert "could not be decrypted" in out
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@pytest.mark.asyncio
|
|
153
|
+
@respx.mock
|
|
154
|
+
async def test_getbased_lab_context_token_alone_cannot_decrypt_v2(gm, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
155
|
+
payload = _encrypted_context_payload(gm, "[section:hormones]\nsecret\n[/section:hormones]", profile_id="abc")
|
|
156
|
+
monkeypatch.setattr(gm, "AGENT_CONTEXT_KEY", "")
|
|
157
|
+
respx.get(GATEWAY_CONTEXT_URL).mock(return_value=Response(200, json=payload))
|
|
158
|
+
|
|
159
|
+
out = await gm.getbased_lab_context()
|
|
160
|
+
|
|
161
|
+
assert "Error:" in out
|
|
162
|
+
assert "GETBASED_AGENT_CONTEXT_KEY not set" in out
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@pytest.mark.asyncio
|
|
166
|
+
@respx.mock
|
|
167
|
+
async def test_getbased_lab_context_rejects_non_utf8_plaintext(gm) -> None:
|
|
168
|
+
import base64
|
|
169
|
+
|
|
170
|
+
profile_id = "abc"
|
|
171
|
+
raw_key = gm._decode_agent_context_key(gm.AGENT_CONTEXT_KEY)
|
|
172
|
+
iv = bytes(range(16, 28))
|
|
173
|
+
aad = gm.AGENT_CONTEXT_AAD_PREFIX + b":" + profile_id.encode("utf-8")
|
|
174
|
+
ciphertext = AESGCM(raw_key).encrypt(iv, b"\xff\xfe\xfd", aad)
|
|
175
|
+
payload = {
|
|
176
|
+
"profileId": profile_id,
|
|
177
|
+
"context": json.dumps({"encryptedContext": {
|
|
178
|
+
"version": 2,
|
|
179
|
+
"alg": "AES-256-GCM",
|
|
180
|
+
"keyDerivation": "raw-256-bit-key",
|
|
181
|
+
"keyId": gm._agent_context_key_id(raw_key),
|
|
182
|
+
"iv": base64.b64encode(iv).decode("ascii"),
|
|
183
|
+
"ciphertext": base64.b64encode(ciphertext).decode("ascii"),
|
|
184
|
+
}}),
|
|
185
|
+
}
|
|
186
|
+
respx.get(GATEWAY_CONTEXT_URL).mock(return_value=Response(200, json=payload))
|
|
187
|
+
|
|
188
|
+
out = await gm.getbased_lab_context()
|
|
189
|
+
|
|
190
|
+
assert "Error:" in out
|
|
191
|
+
assert "invalid UTF-8" in out
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@pytest.mark.asyncio
|
|
195
|
+
@respx.mock
|
|
196
|
+
async def test_getbased_lab_context_keeps_v1_rollout_fallback(gm, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
197
|
+
plaintext = "[section:hormones]\nlegacy secret\n[/section:hormones]"
|
|
198
|
+
payload = _encrypted_context_payload(gm, plaintext, profile_id="abc", version=1)
|
|
199
|
+
monkeypatch.setattr(gm, "AGENT_CONTEXT_KEY", "")
|
|
200
|
+
respx.get(GATEWAY_CONTEXT_URL).mock(return_value=Response(200, json=payload))
|
|
201
|
+
|
|
202
|
+
out = await gm.getbased_lab_context()
|
|
203
|
+
|
|
204
|
+
assert "legacy secret" in out
|
|
205
|
+
|
|
206
|
+
|
|
64
207
|
@pytest.mark.asyncio
|
|
65
208
|
@respx.mock
|
|
66
209
|
async def test_getbased_lab_context_gateway_error(gm) -> None:
|
getbased_mcp-0.2.4/LICENSE
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
GNU GENERAL PUBLIC LICENSE
|
|
2
|
-
Version 3, 29 June 2007
|
|
3
|
-
|
|
4
|
-
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
5
|
-
Everyone is permitted to copy and distribute verbatim copies
|
|
6
|
-
of this license document, but changing it is not allowed.
|
|
7
|
-
|
|
8
|
-
Preamble
|
|
9
|
-
|
|
10
|
-
The GNU General Public License is a free, copyleft license for
|
|
11
|
-
software and other kinds of works.
|
|
12
|
-
|
|
13
|
-
The licenses for most software and other practical works are designed
|
|
14
|
-
to take away your freedom to share and change the works. By contrast,
|
|
15
|
-
the GNU General Public License is intended to guarantee your freedom to
|
|
16
|
-
share and change all versions of a program--to make sure it remains free
|
|
17
|
-
software for all its users. We, the Free Software Foundation, use the
|
|
18
|
-
GNU General Public License for most of our software; it applies also to
|
|
19
|
-
any other work released this way by its authors. You can apply it to
|
|
20
|
-
your programs, too.
|
|
21
|
-
|
|
22
|
-
For the full license text, see <https://www.gnu.org/licenses/gpl-3.0.txt>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|