liberrex-mcp-server 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.
- liberrex_mcp_server-0.1.0/LICENSE +28 -0
- liberrex_mcp_server-0.1.0/PKG-INFO +212 -0
- liberrex_mcp_server-0.1.0/README.md +156 -0
- liberrex_mcp_server-0.1.0/pyproject.toml +45 -0
- liberrex_mcp_server-0.1.0/setup.cfg +4 -0
- liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/__init__.py +10 -0
- liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/__main__.py +3 -0
- liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/copilot.py +66 -0
- liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/keys.py +17 -0
- liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/liberrex/__init__.py +5 -0
- liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/liberrex/cli.py +102 -0
- liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/liberrex/client.py +216 -0
- liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/liberrex/config.py +139 -0
- liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/liberrex/tools.py +373 -0
- liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/mesh.py +135 -0
- liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/peers.py +241 -0
- liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/py.typed +0 -0
- liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/server.py +385 -0
- liberrex_mcp_server-0.1.0/src/liberrex_mcp_server.egg-info/PKG-INFO +212 -0
- liberrex_mcp_server-0.1.0/src/liberrex_mcp_server.egg-info/SOURCES.txt +26 -0
- liberrex_mcp_server-0.1.0/src/liberrex_mcp_server.egg-info/dependency_links.txt +1 -0
- liberrex_mcp_server-0.1.0/src/liberrex_mcp_server.egg-info/entry_points.txt +2 -0
- liberrex_mcp_server-0.1.0/src/liberrex_mcp_server.egg-info/requires.txt +10 -0
- liberrex_mcp_server-0.1.0/src/liberrex_mcp_server.egg-info/top_level.txt +1 -0
- liberrex_mcp_server-0.1.0/tests/test_controls.py +138 -0
- liberrex_mcp_server-0.1.0/tests/test_copilot_adapter.py +78 -0
- liberrex_mcp_server-0.1.0/tests/test_liberrex.py +275 -0
- liberrex_mcp_server-0.1.0/tests/test_mesh.py +192 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Liberrex MCP server — Proprietary License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Liberrex. All rights reserved.
|
|
4
|
+
|
|
5
|
+
1. Grant. Subject to these terms, Liberrex grants you a non-exclusive, non-transferable, revocable license to install and use
|
|
6
|
+
this software, unmodified or modified, solely to integrate your own applications with the Liberrex platform while you
|
|
7
|
+
(or the organisation you act for) hold a valid Liberrex subscription or a written evaluation agreement with Liberrex.
|
|
8
|
+
|
|
9
|
+
2. Restrictions. You may not (a) use this software to access any service other than the Liberrex platform; (b) sell,
|
|
10
|
+
sublicense, rent or distribute it on its own, except as an unmodifiable part of your application delivered to your end
|
|
11
|
+
users; (c) remove or alter this notice; (d) use it to build a product that competes with the Liberrex platform.
|
|
12
|
+
|
|
13
|
+
3. Ownership. Liberrex retains all rights, title and interest in this software. No other rights are granted, by implication
|
|
14
|
+
or otherwise.
|
|
15
|
+
|
|
16
|
+
4. Termination. This license ends automatically when your Liberrex subscription or evaluation ends, or if you breach these
|
|
17
|
+
terms. On termination you must stop using the software.
|
|
18
|
+
|
|
19
|
+
5. No warranty. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING MERCHANTABILITY,
|
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
|
|
21
|
+
|
|
22
|
+
6. Limitation of liability. TO THE MAXIMUM EXTENT PERMITTED BY LAW, LIBERREX SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
|
|
23
|
+
SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM THE USE OF THE SOFTWARE.
|
|
24
|
+
|
|
25
|
+
7. Your agreement with Liberrex. If you have a signed agreement with Liberrex that covers this software, that agreement
|
|
26
|
+
prevails over these terms.
|
|
27
|
+
|
|
28
|
+
Contact: https://ai.liberrex.com
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: liberrex-mcp-server
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Publish a platform's tools on the Liberrex MCP mesh (peer keys, per-call user identity, permissions), plus the Liberrex AI MCP server.
|
|
5
|
+
Author: Liberrex
|
|
6
|
+
License: Liberrex MCP server — Proprietary License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Liberrex. All rights reserved.
|
|
9
|
+
|
|
10
|
+
1. Grant. Subject to these terms, Liberrex grants you a non-exclusive, non-transferable, revocable license to install and use
|
|
11
|
+
this software, unmodified or modified, solely to integrate your own applications with the Liberrex platform while you
|
|
12
|
+
(or the organisation you act for) hold a valid Liberrex subscription or a written evaluation agreement with Liberrex.
|
|
13
|
+
|
|
14
|
+
2. Restrictions. You may not (a) use this software to access any service other than the Liberrex platform; (b) sell,
|
|
15
|
+
sublicense, rent or distribute it on its own, except as an unmodifiable part of your application delivered to your end
|
|
16
|
+
users; (c) remove or alter this notice; (d) use it to build a product that competes with the Liberrex platform.
|
|
17
|
+
|
|
18
|
+
3. Ownership. Liberrex retains all rights, title and interest in this software. No other rights are granted, by implication
|
|
19
|
+
or otherwise.
|
|
20
|
+
|
|
21
|
+
4. Termination. This license ends automatically when your Liberrex subscription or evaluation ends, or if you breach these
|
|
22
|
+
terms. On termination you must stop using the software.
|
|
23
|
+
|
|
24
|
+
5. No warranty. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
|
|
26
|
+
|
|
27
|
+
6. Limitation of liability. TO THE MAXIMUM EXTENT PERMITTED BY LAW, LIBERREX SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
|
|
28
|
+
SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM THE USE OF THE SOFTWARE.
|
|
29
|
+
|
|
30
|
+
7. Your agreement with Liberrex. If you have a signed agreement with Liberrex that covers this software, that agreement
|
|
31
|
+
prevails over these terms.
|
|
32
|
+
|
|
33
|
+
Contact: https://ai.liberrex.com
|
|
34
|
+
|
|
35
|
+
Project-URL: Homepage, https://ai.liberrex.com
|
|
36
|
+
Keywords: liberrex,mcp,model-context-protocol,ai,agents,integration
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
39
|
+
Classifier: Operating System :: OS Independent
|
|
40
|
+
Classifier: Intended Audience :: Developers
|
|
41
|
+
Classifier: Typing :: Typed
|
|
42
|
+
Classifier: License :: Other/Proprietary License
|
|
43
|
+
Requires-Python: >=3.10
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
License-File: LICENSE
|
|
46
|
+
Requires-Dist: mcp<3,>=2.3
|
|
47
|
+
Requires-Dist: httpx>=0.27
|
|
48
|
+
Requires-Dist: pydantic>=2
|
|
49
|
+
Requires-Dist: uvicorn>=0.30
|
|
50
|
+
Provides-Extra: dev
|
|
51
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
52
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
53
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
54
|
+
Requires-Dist: uvicorn>=0.30; extra == "dev"
|
|
55
|
+
Dynamic: license-file
|
|
56
|
+
|
|
57
|
+
# liberrex-mcp-server (Python)
|
|
58
|
+
|
|
59
|
+
Publish a platform's tools on the **Liberrex MCP mesh**, so that Liberrex AI, Animatoria, the ERP and any other
|
|
60
|
+
platform of the ecosystem can call them. It follows the shared [mesh conventions](../../MCP_MESH.md): peer keys,
|
|
61
|
+
per-call user identity, permissions, write control and `[code]` errors. The package also ships the **Liberrex AI
|
|
62
|
+
platform server** (`liberrex-mcp-server`): agents, copilot and sales intelligence (next best action, next best
|
|
63
|
+
product…) as tools.
|
|
64
|
+
|
|
65
|
+
- Python ≥ 3.10, built on the official `mcp` SDK v2. Stateless Streamable HTTP (any instance answers any call) or stdio.
|
|
66
|
+
- Pair it with [`liberrex-mcp-client`](../../mcp-client/python) on the calling side.
|
|
67
|
+
- TypeScript equivalent: [`@liberrex/mcp-server`](../typescript).
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install liberrex-mcp-server
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Publish your platform's tools
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from typing import Optional
|
|
77
|
+
from liberrex_mcp_server import PeerRegistry, PlatformError, PlatformServer, ToolContext
|
|
78
|
+
|
|
79
|
+
server = PlatformServer("erp", title="Acme ERP", peers=PeerRegistry.from_env())
|
|
80
|
+
|
|
81
|
+
@server.tool(permission="invoices.read", category="finance")
|
|
82
|
+
def overdue_invoices(ctx: ToolContext, customer: Optional[str] = None, limit: int = 20) -> dict:
|
|
83
|
+
"""Overdue invoices the user can see, oldest first."""
|
|
84
|
+
user = ctx.require_identity() # who the call acts for: user_id, tenant_id, roles, locale
|
|
85
|
+
rows = invoices.overdue(tenant=user.tenant_id, user=user.user_id, customer=customer, limit=limit)
|
|
86
|
+
if rows is None:
|
|
87
|
+
raise PlatformError("not_found", "Unknown customer") # → "[not_found] Unknown customer"
|
|
88
|
+
return {"items": rows}
|
|
89
|
+
|
|
90
|
+
@server.tool(permission="invoices.write", write=True)
|
|
91
|
+
async def send_reminder(ctx: ToolContext, invoice_id: str) -> dict:
|
|
92
|
+
"""Send a payment reminder."""
|
|
93
|
+
...
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Then serve it:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
app.mount("/mcp", server.asgi()) # FastAPI / Starlette; the endpoint answers at /mcp/
|
|
100
|
+
server.run_http(port=8765) # standalone, at /mcp
|
|
101
|
+
server.run_stdio() # local desktop clients
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The SDK checks these before your function runs:
|
|
105
|
+
|
|
106
|
+
| check | refusal |
|
|
107
|
+
|---|---|
|
|
108
|
+
| The `Authorization: Bearer` key is a valid (not expired) key of a configured peer | HTTP 401 `[unauthorized]` |
|
|
109
|
+
| At most `rate_per_minute × 10` HTTP requests per minute from that peer (flood guard) | HTTP 429 + `Retry-After` |
|
|
110
|
+
| At most `rate_per_minute` tool calls per minute from that peer | `[rate_limited]` |
|
|
111
|
+
| The identity's tenant is one the peer may act in | `[tenant_not_allowed]` |
|
|
112
|
+
| The identity's roles, capped by the peer's `roles`, include the tool's `permission` | `[forbidden]` / `[identity_required]` |
|
|
113
|
+
| `write=True` tools only for peers with `writes` | `[writes_disabled]` |
|
|
114
|
+
| At most 4 platform hops | `[too_many_hops]` |
|
|
115
|
+
| Any other exception: logged, and not leaked to the caller | `[internal_error] <tool> failed` |
|
|
116
|
+
|
|
117
|
+
Sync functions run in a worker thread, so they may block, for example on a database call. Use
|
|
118
|
+
`ctx.forward_meta()` when a tool calls another platform: the same person and trace go along, with `hops + 1`.
|
|
119
|
+
|
|
120
|
+
### Already integrated with the Liberrex copilot?
|
|
121
|
+
|
|
122
|
+
Publish every tool you declared with `liberrex_copilot` in one line. The schemas, permission checks and error codes stay
|
|
123
|
+
the same:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from liberrex_mcp_server.copilot import publish_copilot
|
|
127
|
+
publish_copilot(server, copilot, user=lambda identity: load_user_with_roles(identity))
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
This is how Animatoria publishes its 43 tools at `/mcp`. The `user` callback lets your platform compute the user's
|
|
131
|
+
roles itself, rather than trust the roles the caller sends.
|
|
132
|
+
|
|
133
|
+
### Tools that already have a JSON Schema
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
server.raw_tool("lookup", description="…", input_schema={"type": "object", "properties": {...}},
|
|
137
|
+
handler=lambda ctx, arguments: ...)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Peers (who may call)
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
liberrex-mcp-server new-key liberrex-ai # prints a key for the caller and the sha256 entry for you
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
LIBERREX_MCP_PEERS='{"liberrex-ai": {"keys": [{"key": "sha256:…", "expires": "2027-01-31"}, "sha256:…"],
|
|
148
|
+
"tenants": "*", "writes": false, "rate_per_minute": 300},
|
|
149
|
+
"erp": {"key": "sha256:…", "tenants": ["acme"], "roles": ["invoices.read"]}}'
|
|
150
|
+
LIBERREX_MCP_PEERS_FILE=/etc/erp/mcp-peers.json # or: the same JSON in a file, re-read when it changes
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
| field | meaning | default |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| `key` / `keys` | `sha256:<64 hex>` (preferred) or the raw key (24 characters at least); or `{"key", "expires": "YYYY-MM-DD"}`: valid until the end of that UTC day | required |
|
|
156
|
+
| `tenants` | `"*"` or the tenant ids the peer may act in | `"*"` |
|
|
157
|
+
| `writes` | may call `write=True` tools | `false` |
|
|
158
|
+
| `roles` | ceiling: the identity's roles are intersected with it | none |
|
|
159
|
+
| `rate_per_minute` | tool calls per minute (1 to 10,000), per process; HTTP requests are capped at 10× that | `120` |
|
|
160
|
+
|
|
161
|
+
Keep only hashes on the server, give each caller its own key, and grant `writes` only where needed. Rotate with an
|
|
162
|
+
overlap: add the new hash to `keys`, give the caller the new key, then remove the old hash (or let its `expires` pass).
|
|
163
|
+
`python -m liberrex_mcp_server.keys <peer>` also prints a key and its hash.
|
|
164
|
+
|
|
165
|
+
With `LIBERREX_MCP_PEERS_FILE` (`FilePeerRegistry`), the file is re-read when its mtime changes (checked at most every
|
|
166
|
+
2 s): revoke a caller without a restart. Invalid at startup → `PeerConfigError`; invalid later → logged, the previous
|
|
167
|
+
peers stay; missing → no caller is accepted.
|
|
168
|
+
|
|
169
|
+
### Audit
|
|
170
|
+
|
|
171
|
+
Every tool call (`ok` / `refused` / `error`) and every refused HTTP request (`unauthorized`, `rate_limited`) produces one
|
|
172
|
+
event for `PlatformServer(..., audit=fn)` (default: a JSON line on the `liberrex_mcp_server.audit` logger; `None`: off):
|
|
173
|
+
`at`, `platform`, `event`, `tool`, `write`, `peer`, `user`, `tenant`, `trace_id`, `origin`, `hops`, `status`, `code`,
|
|
174
|
+
`duration_ms` (and `client` for HTTP events). Tool arguments are never included. The fields are the same in the
|
|
175
|
+
TypeScript server; see [MCP_MESH.md §6](../../MCP_MESH.md).
|
|
176
|
+
|
|
177
|
+
## The Liberrex AI platform server
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
liberrex-mcp-server --check # credentials, peers, tools
|
|
181
|
+
liberrex-mcp-server --transport http --port 8765 # the mesh endpoint at /mcp (needs LIBERREX_MCP_PEERS)
|
|
182
|
+
liberrex-mcp-server # stdio for Claude Desktop / Cursor (acts as LIBERREX_USER_ID)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
| Variable | Meaning |
|
|
186
|
+
|---|---|
|
|
187
|
+
| `LIBERREX_API_URL` | Liberrex API (default `https://ai-api.liberrex.com`) |
|
|
188
|
+
| `LIBERREX_API_KEY` | Developer API key `key_id:secret`: agents, conversations, usage (company-wide tools) |
|
|
189
|
+
| `LIBERREX_SECRET_KEY` | Embedded copilot key `lxsk_…`: copilot and surfaces, **for the identity of each call** |
|
|
190
|
+
| `LIBERREX_SURFACES` | `next_best_action=nba-queue,next_best_product=nbp-queue`: modes of `sales_recommendations` |
|
|
191
|
+
| `LIBERREX_MCP_PEERS` / `LIBERREX_MCP_PEERS_FILE` | Platforms allowed to call (see above) |
|
|
192
|
+
| `LIBERREX_USER_ID`, `LIBERREX_TENANT_ID`, `LIBERREX_USER_ROLES`, `LIBERREX_LOCALE` | stdio only: the user a desktop client acts as |
|
|
193
|
+
| `LIBERREX_MCP_READ_ONLY` | `1` leaves out confirm_action, create_action_batch and end_conversation |
|
|
194
|
+
|
|
195
|
+
The 20 tools are:
|
|
196
|
+
|
|
197
|
+
| Group | Tools |
|
|
198
|
+
|---|---|
|
|
199
|
+
| Agents | whoami, list_agents, get_agent, ask_agent, list_copilot_agents, start_agent_group, ask_agent_group, get_agent_group, list_conversations, get_conversation, end_conversation, get_usage |
|
|
200
|
+
| Copilot | copilot_ask, copilot_list_threads, copilot_get_thread |
|
|
201
|
+
| Sales intelligence | sales_recommendations, render_surface, suggest_field, run_surface_action, create_action_batch, confirm_action, cancel_action, record_feedback |
|
|
202
|
+
| Billing | get_copilot_usage |
|
|
203
|
+
|
|
204
|
+
Each calling user gets their own copilot session, minted for the `user.id` and `tenant.id` the calling platform sends.
|
|
205
|
+
Recommendations are therefore computed from that user's own data, under their own roles.
|
|
206
|
+
|
|
207
|
+
## Development
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
python3.12 -m venv .venv && .venv/bin/pip install -e ".[dev]" -e ../../mcp-client/python -e ../../python
|
|
211
|
+
.venv/bin/pytest # 39 tests, including server ↔ client end to end over HTTP
|
|
212
|
+
```
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# liberrex-mcp-server (Python)
|
|
2
|
+
|
|
3
|
+
Publish a platform's tools on the **Liberrex MCP mesh**, so that Liberrex AI, Animatoria, the ERP and any other
|
|
4
|
+
platform of the ecosystem can call them. It follows the shared [mesh conventions](../../MCP_MESH.md): peer keys,
|
|
5
|
+
per-call user identity, permissions, write control and `[code]` errors. The package also ships the **Liberrex AI
|
|
6
|
+
platform server** (`liberrex-mcp-server`): agents, copilot and sales intelligence (next best action, next best
|
|
7
|
+
product…) as tools.
|
|
8
|
+
|
|
9
|
+
- Python ≥ 3.10, built on the official `mcp` SDK v2. Stateless Streamable HTTP (any instance answers any call) or stdio.
|
|
10
|
+
- Pair it with [`liberrex-mcp-client`](../../mcp-client/python) on the calling side.
|
|
11
|
+
- TypeScript equivalent: [`@liberrex/mcp-server`](../typescript).
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install liberrex-mcp-server
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Publish your platform's tools
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from typing import Optional
|
|
21
|
+
from liberrex_mcp_server import PeerRegistry, PlatformError, PlatformServer, ToolContext
|
|
22
|
+
|
|
23
|
+
server = PlatformServer("erp", title="Acme ERP", peers=PeerRegistry.from_env())
|
|
24
|
+
|
|
25
|
+
@server.tool(permission="invoices.read", category="finance")
|
|
26
|
+
def overdue_invoices(ctx: ToolContext, customer: Optional[str] = None, limit: int = 20) -> dict:
|
|
27
|
+
"""Overdue invoices the user can see, oldest first."""
|
|
28
|
+
user = ctx.require_identity() # who the call acts for: user_id, tenant_id, roles, locale
|
|
29
|
+
rows = invoices.overdue(tenant=user.tenant_id, user=user.user_id, customer=customer, limit=limit)
|
|
30
|
+
if rows is None:
|
|
31
|
+
raise PlatformError("not_found", "Unknown customer") # → "[not_found] Unknown customer"
|
|
32
|
+
return {"items": rows}
|
|
33
|
+
|
|
34
|
+
@server.tool(permission="invoices.write", write=True)
|
|
35
|
+
async def send_reminder(ctx: ToolContext, invoice_id: str) -> dict:
|
|
36
|
+
"""Send a payment reminder."""
|
|
37
|
+
...
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Then serve it:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
app.mount("/mcp", server.asgi()) # FastAPI / Starlette; the endpoint answers at /mcp/
|
|
44
|
+
server.run_http(port=8765) # standalone, at /mcp
|
|
45
|
+
server.run_stdio() # local desktop clients
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The SDK checks these before your function runs:
|
|
49
|
+
|
|
50
|
+
| check | refusal |
|
|
51
|
+
|---|---|
|
|
52
|
+
| The `Authorization: Bearer` key is a valid (not expired) key of a configured peer | HTTP 401 `[unauthorized]` |
|
|
53
|
+
| At most `rate_per_minute × 10` HTTP requests per minute from that peer (flood guard) | HTTP 429 + `Retry-After` |
|
|
54
|
+
| At most `rate_per_minute` tool calls per minute from that peer | `[rate_limited]` |
|
|
55
|
+
| The identity's tenant is one the peer may act in | `[tenant_not_allowed]` |
|
|
56
|
+
| The identity's roles, capped by the peer's `roles`, include the tool's `permission` | `[forbidden]` / `[identity_required]` |
|
|
57
|
+
| `write=True` tools only for peers with `writes` | `[writes_disabled]` |
|
|
58
|
+
| At most 4 platform hops | `[too_many_hops]` |
|
|
59
|
+
| Any other exception: logged, and not leaked to the caller | `[internal_error] <tool> failed` |
|
|
60
|
+
|
|
61
|
+
Sync functions run in a worker thread, so they may block, for example on a database call. Use
|
|
62
|
+
`ctx.forward_meta()` when a tool calls another platform: the same person and trace go along, with `hops + 1`.
|
|
63
|
+
|
|
64
|
+
### Already integrated with the Liberrex copilot?
|
|
65
|
+
|
|
66
|
+
Publish every tool you declared with `liberrex_copilot` in one line. The schemas, permission checks and error codes stay
|
|
67
|
+
the same:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from liberrex_mcp_server.copilot import publish_copilot
|
|
71
|
+
publish_copilot(server, copilot, user=lambda identity: load_user_with_roles(identity))
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This is how Animatoria publishes its 43 tools at `/mcp`. The `user` callback lets your platform compute the user's
|
|
75
|
+
roles itself, rather than trust the roles the caller sends.
|
|
76
|
+
|
|
77
|
+
### Tools that already have a JSON Schema
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
server.raw_tool("lookup", description="…", input_schema={"type": "object", "properties": {...}},
|
|
81
|
+
handler=lambda ctx, arguments: ...)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Peers (who may call)
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
liberrex-mcp-server new-key liberrex-ai # prints a key for the caller and the sha256 entry for you
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
LIBERREX_MCP_PEERS='{"liberrex-ai": {"keys": [{"key": "sha256:…", "expires": "2027-01-31"}, "sha256:…"],
|
|
92
|
+
"tenants": "*", "writes": false, "rate_per_minute": 300},
|
|
93
|
+
"erp": {"key": "sha256:…", "tenants": ["acme"], "roles": ["invoices.read"]}}'
|
|
94
|
+
LIBERREX_MCP_PEERS_FILE=/etc/erp/mcp-peers.json # or: the same JSON in a file, re-read when it changes
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
| field | meaning | default |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `key` / `keys` | `sha256:<64 hex>` (preferred) or the raw key (24 characters at least); or `{"key", "expires": "YYYY-MM-DD"}`: valid until the end of that UTC day | required |
|
|
100
|
+
| `tenants` | `"*"` or the tenant ids the peer may act in | `"*"` |
|
|
101
|
+
| `writes` | may call `write=True` tools | `false` |
|
|
102
|
+
| `roles` | ceiling: the identity's roles are intersected with it | none |
|
|
103
|
+
| `rate_per_minute` | tool calls per minute (1 to 10,000), per process; HTTP requests are capped at 10× that | `120` |
|
|
104
|
+
|
|
105
|
+
Keep only hashes on the server, give each caller its own key, and grant `writes` only where needed. Rotate with an
|
|
106
|
+
overlap: add the new hash to `keys`, give the caller the new key, then remove the old hash (or let its `expires` pass).
|
|
107
|
+
`python -m liberrex_mcp_server.keys <peer>` also prints a key and its hash.
|
|
108
|
+
|
|
109
|
+
With `LIBERREX_MCP_PEERS_FILE` (`FilePeerRegistry`), the file is re-read when its mtime changes (checked at most every
|
|
110
|
+
2 s): revoke a caller without a restart. Invalid at startup → `PeerConfigError`; invalid later → logged, the previous
|
|
111
|
+
peers stay; missing → no caller is accepted.
|
|
112
|
+
|
|
113
|
+
### Audit
|
|
114
|
+
|
|
115
|
+
Every tool call (`ok` / `refused` / `error`) and every refused HTTP request (`unauthorized`, `rate_limited`) produces one
|
|
116
|
+
event for `PlatformServer(..., audit=fn)` (default: a JSON line on the `liberrex_mcp_server.audit` logger; `None`: off):
|
|
117
|
+
`at`, `platform`, `event`, `tool`, `write`, `peer`, `user`, `tenant`, `trace_id`, `origin`, `hops`, `status`, `code`,
|
|
118
|
+
`duration_ms` (and `client` for HTTP events). Tool arguments are never included. The fields are the same in the
|
|
119
|
+
TypeScript server; see [MCP_MESH.md §6](../../MCP_MESH.md).
|
|
120
|
+
|
|
121
|
+
## The Liberrex AI platform server
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
liberrex-mcp-server --check # credentials, peers, tools
|
|
125
|
+
liberrex-mcp-server --transport http --port 8765 # the mesh endpoint at /mcp (needs LIBERREX_MCP_PEERS)
|
|
126
|
+
liberrex-mcp-server # stdio for Claude Desktop / Cursor (acts as LIBERREX_USER_ID)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
| Variable | Meaning |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `LIBERREX_API_URL` | Liberrex API (default `https://ai-api.liberrex.com`) |
|
|
132
|
+
| `LIBERREX_API_KEY` | Developer API key `key_id:secret`: agents, conversations, usage (company-wide tools) |
|
|
133
|
+
| `LIBERREX_SECRET_KEY` | Embedded copilot key `lxsk_…`: copilot and surfaces, **for the identity of each call** |
|
|
134
|
+
| `LIBERREX_SURFACES` | `next_best_action=nba-queue,next_best_product=nbp-queue`: modes of `sales_recommendations` |
|
|
135
|
+
| `LIBERREX_MCP_PEERS` / `LIBERREX_MCP_PEERS_FILE` | Platforms allowed to call (see above) |
|
|
136
|
+
| `LIBERREX_USER_ID`, `LIBERREX_TENANT_ID`, `LIBERREX_USER_ROLES`, `LIBERREX_LOCALE` | stdio only: the user a desktop client acts as |
|
|
137
|
+
| `LIBERREX_MCP_READ_ONLY` | `1` leaves out confirm_action, create_action_batch and end_conversation |
|
|
138
|
+
|
|
139
|
+
The 20 tools are:
|
|
140
|
+
|
|
141
|
+
| Group | Tools |
|
|
142
|
+
|---|---|
|
|
143
|
+
| Agents | whoami, list_agents, get_agent, ask_agent, list_copilot_agents, start_agent_group, ask_agent_group, get_agent_group, list_conversations, get_conversation, end_conversation, get_usage |
|
|
144
|
+
| Copilot | copilot_ask, copilot_list_threads, copilot_get_thread |
|
|
145
|
+
| Sales intelligence | sales_recommendations, render_surface, suggest_field, run_surface_action, create_action_batch, confirm_action, cancel_action, record_feedback |
|
|
146
|
+
| Billing | get_copilot_usage |
|
|
147
|
+
|
|
148
|
+
Each calling user gets their own copilot session, minted for the `user.id` and `tenant.id` the calling platform sends.
|
|
149
|
+
Recommendations are therefore computed from that user's own data, under their own roles.
|
|
150
|
+
|
|
151
|
+
## Development
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
python3.12 -m venv .venv && .venv/bin/pip install -e ".[dev]" -e ../../mcp-client/python -e ../../python
|
|
155
|
+
.venv/bin/pytest # 39 tests, including server ↔ client end to end over HTTP
|
|
156
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "liberrex-mcp-server"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Publish a platform's tools on the Liberrex MCP mesh (peer keys, per-call user identity, permissions), plus the Liberrex AI MCP server."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [{ name = "Liberrex" }]
|
|
13
|
+
keywords = ["liberrex", "mcp", "model-context-protocol", "ai", "agents", "integration"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
"License :: Other/Proprietary License",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["mcp>=2.3,<3", "httpx>=0.27", "pydantic>=2", "uvicorn>=0.30"]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = ["pytest>=7", "pytest-asyncio>=0.23", "respx>=0.21", "uvicorn>=0.30"]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
liberrex-mcp-server = "liberrex_mcp_server.liberrex.cli:main"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://ai.liberrex.com"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools]
|
|
34
|
+
package-dir = { "" = "src" }
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["src"]
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.package-data]
|
|
40
|
+
liberrex_mcp_server = ["py.typed"]
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
testpaths = ["tests"]
|
|
44
|
+
addopts = "-q"
|
|
45
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""liberrex-mcp-server — publish a platform's tools on the Liberrex MCP mesh (and the Liberrex AI server itself)."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from .mesh import META_IDENTITY, META_TOOL, META_TRACE, Identity, PlatformError, Trace # noqa: E402
|
|
6
|
+
from .peers import Peer, PeerConfigError, PeerRegistry, new_key # noqa: E402
|
|
7
|
+
from .server import PlatformServer, ToolContext # noqa: E402
|
|
8
|
+
|
|
9
|
+
__all__ = ["__version__", "PlatformServer", "ToolContext", "Identity", "Trace", "PlatformError", "Peer", "PeerRegistry",
|
|
10
|
+
"PeerConfigError", "new_key", "META_IDENTITY", "META_TRACE", "META_TOOL"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Publish the tools a platform already declared for the Liberrex copilot (``liberrex_copilot.Copilot``) on the mesh.
|
|
3
|
+
|
|
4
|
+
from liberrex_copilot import Copilot
|
|
5
|
+
from liberrex_mcp_server import PlatformServer, PeerRegistry
|
|
6
|
+
from liberrex_mcp_server.copilot import publish_copilot
|
|
7
|
+
|
|
8
|
+
server = PlatformServer("animatoria", peers=PeerRegistry.from_env())
|
|
9
|
+
publish_copilot(server, copilot, user=lambda identity: {...}) # every copilot tool, same schema, same checks
|
|
10
|
+
|
|
11
|
+
Each mesh call becomes a local copilot call (``copilot.execute``) for the identity it carries: the copilot's own
|
|
12
|
+
permission check (``tool.permission`` against the user's roles), argument coercion, result size limits and error codes
|
|
13
|
+
apply unchanged. ``user(identity)`` maps the mesh identity to the copilot user — typically the platform loads the user
|
|
14
|
+
and computes its roles itself instead of trusting roles sent by the caller. Write tools (``kind="write"``) are mesh
|
|
15
|
+
write tools: peers need ``writes``, and the caller must have the human's approval (there is no confirmation card here).
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any, Callable, Dict, Iterable, Optional
|
|
20
|
+
|
|
21
|
+
from .mesh import ERROR_CODES, Identity, PlatformError
|
|
22
|
+
from .server import PlatformServer, ToolContext
|
|
23
|
+
|
|
24
|
+
CODE_MAP = {"invalid_arguments": "invalid_request", "unknown_tool": "not_found", "timeout": "upstream_error",
|
|
25
|
+
"not_configured": "upstream_error", "ambiguous": "invalid_request"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def default_user(identity: Identity) -> Dict[str, Any]:
|
|
29
|
+
user: Dict[str, Any] = {"id": identity.user_id, "roles": list(identity.roles)}
|
|
30
|
+
for key in ("name", "email", "locale"):
|
|
31
|
+
if getattr(identity, key):
|
|
32
|
+
user[key] = getattr(identity, key)
|
|
33
|
+
return user
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def publish_copilot(server: PlatformServer, copilot: Any, *, user: Optional[Callable[[Identity], Dict[str, Any]]] = None,
|
|
37
|
+
include: Optional[Iterable[str]] = None, exclude: Iterable[str] = (), writes: bool = True) -> list:
|
|
38
|
+
"""Register the copilot's enabled tools on ``server``; returns their names. ``writes=False`` leaves out write tools."""
|
|
39
|
+
to_user = user or default_user
|
|
40
|
+
wanted = set(include) if include is not None else None
|
|
41
|
+
skipped = set(exclude)
|
|
42
|
+
names = []
|
|
43
|
+
for tool in copilot.tools.values():
|
|
44
|
+
if not tool.enabled or tool.name in skipped or (wanted is not None and tool.name not in wanted):
|
|
45
|
+
continue
|
|
46
|
+
write = tool.kind == "write"
|
|
47
|
+
if write and not writes:
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
def handler(ctx: ToolContext, arguments: Dict[str, Any], _name: str = tool.name) -> Any:
|
|
51
|
+
identity = ctx.require_identity()
|
|
52
|
+
call = {"call_id": ctx.trace.id or "mesh", "tool": _name, "arguments": arguments, "user": to_user(identity),
|
|
53
|
+
"tenant": {"id": identity.tenant_id, **({"name": identity.tenant_name} if identity.tenant_name else {})},
|
|
54
|
+
"session_id": "mesh:%s" % (ctx.peer.name), "thread_id": None, "locale": identity.locale}
|
|
55
|
+
result = copilot.execute(call)
|
|
56
|
+
if not result.get("ok"):
|
|
57
|
+
error = result.get("error") or {}
|
|
58
|
+
code = CODE_MAP.get(error.get("code"), error.get("code"))
|
|
59
|
+
raise PlatformError(code if code in ERROR_CODES else "upstream_error", error.get("message") or "The tool failed")
|
|
60
|
+
data = result.get("data")
|
|
61
|
+
return data if data is not None else {"ok": True}
|
|
62
|
+
|
|
63
|
+
server.raw_tool(tool.name, description=tool.description or tool.name, input_schema=tool.input_schema, handler=handler,
|
|
64
|
+
write=write, destructive=write, category=tool.output_hint)
|
|
65
|
+
names.append(tool.name)
|
|
66
|
+
return names
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""``python -m liberrex_mcp_server.keys [peer-name]``: a new peer key and the hash to configure on the server."""
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from .peers import new_key
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> int:
|
|
9
|
+
name = sys.argv[1] if len(sys.argv) > 1 else "my-platform"
|
|
10
|
+
key, digest = new_key()
|
|
11
|
+
print("Give this key to %s (its LIBERREX_MCP_PLATFORMS entry), it is shown once:\n %s\n" % (name, key))
|
|
12
|
+
print("Add this to the server's LIBERREX_MCP_PEERS:\n %s" % json.dumps({name: {"key": digest, "tenants": "*", "writes": False}}))
|
|
13
|
+
return 0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if __name__ == "__main__":
|
|
17
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"""The Liberrex AI platform server (agents, copilot, sales intelligence) built on PlatformServer."""
|
|
2
|
+
|
|
3
|
+
from .client import LiberrexClient, LiberrexError # noqa: F401
|
|
4
|
+
from .config import ConfigurationError, Settings # noqa: F401
|
|
5
|
+
from .tools import PLATFORM, build_server # noqa: F401
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
``liberrex-mcp-server``: run the Liberrex AI platform server.
|
|
3
|
+
|
|
4
|
+
liberrex-mcp-server stdio, for a desktop MCP client (acts as LIBERREX_USER_ID / LIBERREX_TENANT_ID)
|
|
5
|
+
liberrex-mcp-server --transport http the mesh endpoint at /mcp, for the other platforms (LIBERREX_MCP_PEERS)
|
|
6
|
+
liberrex-mcp-server --check check the credentials, list the tools
|
|
7
|
+
liberrex-mcp-server new-key <platform> a key for a calling platform and the hash to put in LIBERREX_MCP_PEERS
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
from typing import Optional, Sequence
|
|
18
|
+
|
|
19
|
+
from .. import __version__
|
|
20
|
+
from ..peers import PeerConfigError, PeerRegistry, new_key, peers_dict
|
|
21
|
+
from .client import LiberrexClient, LiberrexError
|
|
22
|
+
from .config import ConfigurationError, Settings, mask
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _parser() -> argparse.ArgumentParser:
|
|
26
|
+
parser = argparse.ArgumentParser(prog="liberrex-mcp-server",
|
|
27
|
+
description="Liberrex AI on the Liberrex MCP mesh. Configuration: LIBERREX_* environment variables.")
|
|
28
|
+
parser.add_argument("command", nargs="?", choices=("serve", "new-key"), default="serve")
|
|
29
|
+
parser.add_argument("name", nargs="?", help="new-key: the calling platform's name")
|
|
30
|
+
parser.add_argument("--transport", choices=("stdio", "http"), default=os.environ.get("LIBERREX_MCP_TRANSPORT", "stdio"))
|
|
31
|
+
parser.add_argument("--host", default=os.environ.get("LIBERREX_MCP_HOST", "127.0.0.1"))
|
|
32
|
+
parser.add_argument("--port", type=int, default=int(os.environ.get("LIBERREX_MCP_PORT", "8765")))
|
|
33
|
+
parser.add_argument("--path", default="/mcp", help="HTTP path of the MCP endpoint")
|
|
34
|
+
parser.add_argument("--base-url", help="Liberrex API URL (default: LIBERREX_API_URL or https://ai-api.liberrex.com)")
|
|
35
|
+
parser.add_argument("--read-only", action="store_true", default=None, help="Leave out the tools that change data")
|
|
36
|
+
parser.add_argument("--check", action="store_true", help="Check the credentials and peers, list the tools, and exit")
|
|
37
|
+
parser.add_argument("--version", action="version", version="liberrex-mcp-server " + __version__)
|
|
38
|
+
return parser
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
42
|
+
args = _parser().parse_args(argv)
|
|
43
|
+
logging.basicConfig(level=os.environ.get("LIBERREX_MCP_LOG_LEVEL", "WARNING"), stream=sys.stderr,
|
|
44
|
+
format="%(asctime)s %(name)s %(levelname)s %(message)s")
|
|
45
|
+
if args.command == "new-key":
|
|
46
|
+
key, digest = new_key()
|
|
47
|
+
name = args.name or "my-platform"
|
|
48
|
+
print("Key for %s (shown once; put it in its LIBERREX_MCP_PLATFORMS entry):\n %s\n" % (name, key))
|
|
49
|
+
print("Entry for this server's LIBERREX_MCP_PEERS:\n %s" % json.dumps({name: {"key": digest, "tenants": "*", "writes": False}}))
|
|
50
|
+
return 0
|
|
51
|
+
try:
|
|
52
|
+
settings = Settings.from_env(base_url=args.base_url, read_only=args.read_only)
|
|
53
|
+
peers = PeerRegistry.from_env()
|
|
54
|
+
except (ConfigurationError, PeerConfigError) as failure:
|
|
55
|
+
print("liberrex-mcp-server: " + str(failure), file=sys.stderr)
|
|
56
|
+
return 2
|
|
57
|
+
from .tools import build_server
|
|
58
|
+
server = build_server(settings, peers)
|
|
59
|
+
if args.check:
|
|
60
|
+
return asyncio.run(_check(settings, peers, server))
|
|
61
|
+
if args.transport == "stdio":
|
|
62
|
+
server.run_stdio()
|
|
63
|
+
return 0
|
|
64
|
+
if not len(peers):
|
|
65
|
+
print("liberrex-mcp-server: set LIBERREX_MCP_PEERS (the platforms allowed to call this server)", file=sys.stderr)
|
|
66
|
+
return 2
|
|
67
|
+
print("liberrex-mcp-server %s on http://%s:%s%s — peers: %s" % (
|
|
68
|
+
__version__, args.host, args.port, args.path, ", ".join(p.name for p in peers)), file=sys.stderr)
|
|
69
|
+
server.run_http(args.host, args.port, args.path)
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def _check(settings: Settings, peers: PeerRegistry, server) -> int:
|
|
74
|
+
ok = True
|
|
75
|
+
print("Liberrex API: %s" % settings.base_url)
|
|
76
|
+
async with LiberrexClient(settings) as client:
|
|
77
|
+
if client.api:
|
|
78
|
+
try:
|
|
79
|
+
me = await client.api.request("GET", "/me")
|
|
80
|
+
print("✓ developer API key %s — %s (%s), plan %s" % (
|
|
81
|
+
mask(settings.api_key), (me.get("company") or {}).get("name"), me["owner"]["email"], me.get("plan")))
|
|
82
|
+
except LiberrexError as failure:
|
|
83
|
+
ok = False
|
|
84
|
+
print("✗ developer API key %s — %s" % (mask(settings.api_key), failure))
|
|
85
|
+
if client.copilot:
|
|
86
|
+
identity = settings.default_identity
|
|
87
|
+
if identity:
|
|
88
|
+
try:
|
|
89
|
+
await client.copilot.token(identity)
|
|
90
|
+
print("✓ copilot key %s — session for %s in tenant %s" % (mask(settings.secret_key), identity.user_id, identity.tenant_id))
|
|
91
|
+
except LiberrexError as failure:
|
|
92
|
+
ok = False
|
|
93
|
+
print("✗ copilot key %s — %s" % (mask(settings.secret_key), failure))
|
|
94
|
+
else:
|
|
95
|
+
print("· copilot key %s — not checked (no LIBERREX_USER_ID / LIBERREX_TENANT_ID; mesh calls bring their own user)"
|
|
96
|
+
% mask(settings.secret_key))
|
|
97
|
+
for mode, key in settings.surfaces.items():
|
|
98
|
+
print(" mode %s → surface %s" % (mode, key))
|
|
99
|
+
print("peers: %s" % (json.dumps(peers_dict(peers)) if len(peers) else "none (stdio only)"))
|
|
100
|
+
tools = await server.mcp.list_tools()
|
|
101
|
+
print("%d tools: %s" % (len(tools), ", ".join(t.name for t in tools)))
|
|
102
|
+
return 0 if ok else 1
|