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.
Files changed (28) hide show
  1. liberrex_mcp_server-0.1.0/LICENSE +28 -0
  2. liberrex_mcp_server-0.1.0/PKG-INFO +212 -0
  3. liberrex_mcp_server-0.1.0/README.md +156 -0
  4. liberrex_mcp_server-0.1.0/pyproject.toml +45 -0
  5. liberrex_mcp_server-0.1.0/setup.cfg +4 -0
  6. liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/__init__.py +10 -0
  7. liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/__main__.py +3 -0
  8. liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/copilot.py +66 -0
  9. liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/keys.py +17 -0
  10. liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/liberrex/__init__.py +5 -0
  11. liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/liberrex/cli.py +102 -0
  12. liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/liberrex/client.py +216 -0
  13. liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/liberrex/config.py +139 -0
  14. liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/liberrex/tools.py +373 -0
  15. liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/mesh.py +135 -0
  16. liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/peers.py +241 -0
  17. liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/py.typed +0 -0
  18. liberrex_mcp_server-0.1.0/src/liberrex_mcp_server/server.py +385 -0
  19. liberrex_mcp_server-0.1.0/src/liberrex_mcp_server.egg-info/PKG-INFO +212 -0
  20. liberrex_mcp_server-0.1.0/src/liberrex_mcp_server.egg-info/SOURCES.txt +26 -0
  21. liberrex_mcp_server-0.1.0/src/liberrex_mcp_server.egg-info/dependency_links.txt +1 -0
  22. liberrex_mcp_server-0.1.0/src/liberrex_mcp_server.egg-info/entry_points.txt +2 -0
  23. liberrex_mcp_server-0.1.0/src/liberrex_mcp_server.egg-info/requires.txt +10 -0
  24. liberrex_mcp_server-0.1.0/src/liberrex_mcp_server.egg-info/top_level.txt +1 -0
  25. liberrex_mcp_server-0.1.0/tests/test_controls.py +138 -0
  26. liberrex_mcp_server-0.1.0/tests/test_copilot_adapter.py +78 -0
  27. liberrex_mcp_server-0.1.0/tests/test_liberrex.py +275 -0
  28. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,3 @@
1
+ from .liberrex.cli import main
2
+
3
+ raise SystemExit(main())
@@ -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