inventree-mcp 0.3.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.
- inventree_mcp-0.3.0/LICENSE +21 -0
- inventree_mcp-0.3.0/MANIFEST.in +1 -0
- inventree_mcp-0.3.0/PKG-INFO +140 -0
- inventree_mcp-0.3.0/README.md +121 -0
- inventree_mcp-0.3.0/inventree_mcp/__init__.py +1 -0
- inventree_mcp-0.3.0/inventree_mcp/_well_known_compat.py +22 -0
- inventree_mcp-0.3.0/inventree_mcp/context.py +98 -0
- inventree_mcp-0.3.0/inventree_mcp/core.py +67 -0
- inventree_mcp-0.3.0/inventree_mcp/expand_introspection.py +49 -0
- inventree_mcp-0.3.0/inventree_mcp/filter_introspection.py +189 -0
- inventree_mcp-0.3.0/inventree_mcp/mcp_server.py +73 -0
- inventree_mcp-0.3.0/inventree_mcp/mcp_transport.py +222 -0
- inventree_mcp-0.3.0/inventree_mcp/oauth2_bridge.py +116 -0
- inventree_mcp-0.3.0/inventree_mcp/output_schemas.py +148 -0
- inventree_mcp-0.3.0/inventree_mcp/proxy.py +283 -0
- inventree_mcp-0.3.0/inventree_mcp/schema_introspection.py +163 -0
- inventree_mcp-0.3.0/inventree_mcp/server_card.py +68 -0
- inventree_mcp-0.3.0/inventree_mcp/settings.py +36 -0
- inventree_mcp-0.3.0/inventree_mcp/test_mcp.py +2491 -0
- inventree_mcp-0.3.0/inventree_mcp/tool_logging.py +136 -0
- inventree_mcp-0.3.0/inventree_mcp/tool_visibility.py +196 -0
- inventree_mcp-0.3.0/inventree_mcp/tools/__init__.py +5 -0
- inventree_mcp-0.3.0/inventree_mcp/tools/_common.py +48 -0
- inventree_mcp-0.3.0/inventree_mcp/tools/attachments.py +119 -0
- inventree_mcp-0.3.0/inventree_mcp/tools/bom.py +211 -0
- inventree_mcp-0.3.0/inventree_mcp/tools/build_orders.py +264 -0
- inventree_mcp-0.3.0/inventree_mcp/tools/categories.py +90 -0
- inventree_mcp-0.3.0/inventree_mcp/tools/companies.py +245 -0
- inventree_mcp-0.3.0/inventree_mcp/tools/discovery.py +358 -0
- inventree_mcp-0.3.0/inventree_mcp/tools/locations.py +90 -0
- inventree_mcp-0.3.0/inventree_mcp/tools/parameters.py +206 -0
- inventree_mcp-0.3.0/inventree_mcp/tools/parts.py +95 -0
- inventree_mcp-0.3.0/inventree_mcp/tools/project_codes.py +94 -0
- inventree_mcp-0.3.0/inventree_mcp/tools/purchase_orders.py +193 -0
- inventree_mcp-0.3.0/inventree_mcp/tools/return_orders.py +192 -0
- inventree_mcp-0.3.0/inventree_mcp/tools/sales_orders.py +286 -0
- inventree_mcp-0.3.0/inventree_mcp/tools/stock.py +99 -0
- inventree_mcp-0.3.0/inventree_mcp/tools/stock_history.py +202 -0
- inventree_mcp-0.3.0/inventree_mcp/tools/supplier_parts.py +184 -0
- inventree_mcp-0.3.0/inventree_mcp/view_resolution.py +119 -0
- inventree_mcp-0.3.0/inventree_mcp.egg-info/PKG-INFO +140 -0
- inventree_mcp-0.3.0/inventree_mcp.egg-info/SOURCES.txt +47 -0
- inventree_mcp-0.3.0/inventree_mcp.egg-info/dependency_links.txt +1 -0
- inventree_mcp-0.3.0/inventree_mcp.egg-info/entry_points.txt +2 -0
- inventree_mcp-0.3.0/inventree_mcp.egg-info/requires.txt +4 -0
- inventree_mcp-0.3.0/inventree_mcp.egg-info/top_level.txt +2 -0
- inventree_mcp-0.3.0/pyproject.toml +61 -0
- inventree_mcp-0.3.0/setup.cfg +19 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oliver Walters <admin@inventree.org>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
recursive-include inventree_mcp/static *
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: inventree-mcp
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: MCP server for InvenTree
|
|
5
|
+
Author-email: Oliver Walters <admin@inventree.org>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/inventree/inventree-mcp
|
|
8
|
+
Keywords: inventree,plugin
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Framework :: InvenTree
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: mcp<3,>=2.0
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: coverage; extra == "test"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# InvenTreeMCP
|
|
21
|
+
|
|
22
|
+
[](https://pypi.org/project/inventree-mcp/)
|
|
23
|
+
[](https://github.com/inventree/inventree-mcp/actions/workflows/ci.yaml)
|
|
24
|
+
[](https://codecov.io/gh/inventree/inventree-mcp)
|
|
25
|
+
|
|
26
|
+
An MCP (Model Context Protocol) server for InvenTree, exposed as an InvenTree plugin. It lets MCP
|
|
27
|
+
clients (Claude, other MCP-aware agents) query InvenTree inventory data over a Streamable HTTP
|
|
28
|
+
endpoint.
|
|
29
|
+
|
|
30
|
+
## Design
|
|
31
|
+
|
|
32
|
+
Every tool is a thin wrapper around InvenTree's own REST API view classes (see
|
|
33
|
+
[`inventree_mcp/proxy.py`](inventree_mcp/proxy.py)), dispatched as the authenticated caller - MCP
|
|
34
|
+
requests go through exactly the same permission checks, filtering, and serialization as the regular
|
|
35
|
+
REST API. Tool code never queries the Django ORM directly.
|
|
36
|
+
|
|
37
|
+
Currently read-only, covering parts, stock items/locations, part categories, purchase/sales/return/
|
|
38
|
+
build orders (with line items and allocations), companies, contacts, addresses, manufacturer/
|
|
39
|
+
supplier parts, BOM items, attachments, parameters, stock tracking history, test results, and
|
|
40
|
+
project codes. Once write tools land, the `MCP_READ_ONLY` setting (see Configuration) will block
|
|
41
|
+
them by default regardless of the calling user's permissions.
|
|
42
|
+
|
|
43
|
+
Each tool's `outputSchema` and filter/ordering options are derived live from InvenTree's own
|
|
44
|
+
serializers and views (not hand-maintained), so they can't drift as InvenTree evolves. Call
|
|
45
|
+
`describe_filters(resource)` to see what's available for a given resource. The set of tools an MCP
|
|
46
|
+
client sees is also filtered to what the calling user can actually use - though every call is still
|
|
47
|
+
permission-checked in full regardless of what was advertised.
|
|
48
|
+
|
|
49
|
+
## Setup
|
|
50
|
+
|
|
51
|
+
### 1. Install the plugin
|
|
52
|
+
|
|
53
|
+
Install via the InvenTree plugin manager, or via pip:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install inventree-mcp
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Then enable the plugin under **Admin > Plugins**, and configure its settings (see
|
|
60
|
+
Configuration below).
|
|
61
|
+
|
|
62
|
+
### 2. Create a token for your MCP client
|
|
63
|
+
|
|
64
|
+
Create an InvenTree API token for your MCP client.
|
|
65
|
+
|
|
66
|
+
### 3. Configure your MCP client
|
|
67
|
+
|
|
68
|
+
The endpoint is `<your-inventree-server>/plugin/inventree-mcp/mcp/`, using Streamable HTTP
|
|
69
|
+
transport with an `Authorization: Token <token>` header.
|
|
70
|
+
|
|
71
|
+
For a client that supports remote Streamable HTTP servers directly, add:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"mcpServers": {
|
|
76
|
+
"inventree": {
|
|
77
|
+
"url": "https://<your-inventree-server>/plugin/inventree-mcp/mcp/",
|
|
78
|
+
"headers": {
|
|
79
|
+
"Authorization": "Token <your-api-token>"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
For a client that only supports local (stdio) servers, bridge it with
|
|
87
|
+
[`mcp-remote`](https://www.npmjs.com/package/mcp-remote)
|
|
88
|
+
|
|
89
|
+
*Note: You will need to have node available on your system path*
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"mcpServers": {
|
|
94
|
+
"inventree": {
|
|
95
|
+
"command": "npx",
|
|
96
|
+
"args": [
|
|
97
|
+
"-y",
|
|
98
|
+
"mcp-remote@latest",
|
|
99
|
+
"https://<your-inventree-server>/plugin/inventree-mcp/mcp/",
|
|
100
|
+
"--header",
|
|
101
|
+
"Authorization: Token <your-api-token>"
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
*Note: On Windows, you may need to substitute `npx` with `C:\\PROGRA~1\\nodejs\\npx.cmd` in the command field.*
|
|
109
|
+
|
|
110
|
+
In a setup where the server is running a self-signed certificate, you may need to use the following `env` arguments to disable certificate verification:
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
"args": [
|
|
114
|
+
...
|
|
115
|
+
],
|
|
116
|
+
"env": {
|
|
117
|
+
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Configuration
|
|
122
|
+
|
|
123
|
+
Under **Settings > Plugin Settings**:
|
|
124
|
+
|
|
125
|
+
- **Require Authentication** (`REQUIRE_AUTH`, default `True`): reject unauthenticated requests.
|
|
126
|
+
Only disable for local testing.
|
|
127
|
+
- **Read Only** (`MCP_READ_ONLY`, default `True`): block all write actions via MCP, regardless of
|
|
128
|
+
the calling user's permissions. A plugin-wide kill switch, independent of per-user roles.
|
|
129
|
+
|
|
130
|
+
## Authentication
|
|
131
|
+
|
|
132
|
+
Access follows the calling user's normal InvenTree role assignments. Supported auth methods:
|
|
133
|
+
|
|
134
|
+
- An InvenTree API token: `Authorization: Token <token>`.
|
|
135
|
+
- Basic auth (username/password).
|
|
136
|
+
- An OAuth2 bearer token: `Authorization: Bearer <token>`. A scoped token (e.g. `r:view:part`)
|
|
137
|
+
narrows access *below* the underlying user's roles - useful for issuing an agent a tightly-scoped
|
|
138
|
+
token without creating a separate low-privilege user.
|
|
139
|
+
|
|
140
|
+
Session/cookie auth is not supported (not meaningful for a machine client).
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# InvenTreeMCP
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/inventree-mcp/)
|
|
4
|
+
[](https://github.com/inventree/inventree-mcp/actions/workflows/ci.yaml)
|
|
5
|
+
[](https://codecov.io/gh/inventree/inventree-mcp)
|
|
6
|
+
|
|
7
|
+
An MCP (Model Context Protocol) server for InvenTree, exposed as an InvenTree plugin. It lets MCP
|
|
8
|
+
clients (Claude, other MCP-aware agents) query InvenTree inventory data over a Streamable HTTP
|
|
9
|
+
endpoint.
|
|
10
|
+
|
|
11
|
+
## Design
|
|
12
|
+
|
|
13
|
+
Every tool is a thin wrapper around InvenTree's own REST API view classes (see
|
|
14
|
+
[`inventree_mcp/proxy.py`](inventree_mcp/proxy.py)), dispatched as the authenticated caller - MCP
|
|
15
|
+
requests go through exactly the same permission checks, filtering, and serialization as the regular
|
|
16
|
+
REST API. Tool code never queries the Django ORM directly.
|
|
17
|
+
|
|
18
|
+
Currently read-only, covering parts, stock items/locations, part categories, purchase/sales/return/
|
|
19
|
+
build orders (with line items and allocations), companies, contacts, addresses, manufacturer/
|
|
20
|
+
supplier parts, BOM items, attachments, parameters, stock tracking history, test results, and
|
|
21
|
+
project codes. Once write tools land, the `MCP_READ_ONLY` setting (see Configuration) will block
|
|
22
|
+
them by default regardless of the calling user's permissions.
|
|
23
|
+
|
|
24
|
+
Each tool's `outputSchema` and filter/ordering options are derived live from InvenTree's own
|
|
25
|
+
serializers and views (not hand-maintained), so they can't drift as InvenTree evolves. Call
|
|
26
|
+
`describe_filters(resource)` to see what's available for a given resource. The set of tools an MCP
|
|
27
|
+
client sees is also filtered to what the calling user can actually use - though every call is still
|
|
28
|
+
permission-checked in full regardless of what was advertised.
|
|
29
|
+
|
|
30
|
+
## Setup
|
|
31
|
+
|
|
32
|
+
### 1. Install the plugin
|
|
33
|
+
|
|
34
|
+
Install via the InvenTree plugin manager, or via pip:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install inventree-mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Then enable the plugin under **Admin > Plugins**, and configure its settings (see
|
|
41
|
+
Configuration below).
|
|
42
|
+
|
|
43
|
+
### 2. Create a token for your MCP client
|
|
44
|
+
|
|
45
|
+
Create an InvenTree API token for your MCP client.
|
|
46
|
+
|
|
47
|
+
### 3. Configure your MCP client
|
|
48
|
+
|
|
49
|
+
The endpoint is `<your-inventree-server>/plugin/inventree-mcp/mcp/`, using Streamable HTTP
|
|
50
|
+
transport with an `Authorization: Token <token>` header.
|
|
51
|
+
|
|
52
|
+
For a client that supports remote Streamable HTTP servers directly, add:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"mcpServers": {
|
|
57
|
+
"inventree": {
|
|
58
|
+
"url": "https://<your-inventree-server>/plugin/inventree-mcp/mcp/",
|
|
59
|
+
"headers": {
|
|
60
|
+
"Authorization": "Token <your-api-token>"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
For a client that only supports local (stdio) servers, bridge it with
|
|
68
|
+
[`mcp-remote`](https://www.npmjs.com/package/mcp-remote)
|
|
69
|
+
|
|
70
|
+
*Note: You will need to have node available on your system path*
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"mcpServers": {
|
|
75
|
+
"inventree": {
|
|
76
|
+
"command": "npx",
|
|
77
|
+
"args": [
|
|
78
|
+
"-y",
|
|
79
|
+
"mcp-remote@latest",
|
|
80
|
+
"https://<your-inventree-server>/plugin/inventree-mcp/mcp/",
|
|
81
|
+
"--header",
|
|
82
|
+
"Authorization: Token <your-api-token>"
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
*Note: On Windows, you may need to substitute `npx` with `C:\\PROGRA~1\\nodejs\\npx.cmd` in the command field.*
|
|
90
|
+
|
|
91
|
+
In a setup where the server is running a self-signed certificate, you may need to use the following `env` arguments to disable certificate verification:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
"args": [
|
|
95
|
+
...
|
|
96
|
+
],
|
|
97
|
+
"env": {
|
|
98
|
+
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Configuration
|
|
103
|
+
|
|
104
|
+
Under **Settings > Plugin Settings**:
|
|
105
|
+
|
|
106
|
+
- **Require Authentication** (`REQUIRE_AUTH`, default `True`): reject unauthenticated requests.
|
|
107
|
+
Only disable for local testing.
|
|
108
|
+
- **Read Only** (`MCP_READ_ONLY`, default `True`): block all write actions via MCP, regardless of
|
|
109
|
+
the calling user's permissions. A plugin-wide kill switch, independent of per-user roles.
|
|
110
|
+
|
|
111
|
+
## Authentication
|
|
112
|
+
|
|
113
|
+
Access follows the calling user's normal InvenTree role assignments. Supported auth methods:
|
|
114
|
+
|
|
115
|
+
- An InvenTree API token: `Authorization: Token <token>`.
|
|
116
|
+
- Basic auth (username/password).
|
|
117
|
+
- An OAuth2 bearer token: `Authorization: Bearer <token>`. A scoped token (e.g. `r:view:part`)
|
|
118
|
+
narrows access *below* the underlying user's roles - useful for issuing an agent a tightly-scoped
|
|
119
|
+
token without creating a separate low-privilege user.
|
|
120
|
+
|
|
121
|
+
Session/cookie auth is not supported (not meaningful for a machine client).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PLUGIN_VERSION = "0.3.0"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Compat shim for InvenTree versions that don't yet provide WellKnownMixin.
|
|
2
|
+
|
|
3
|
+
WellKnownMixin (letting a plugin advertise entries under /.well-known/) was
|
|
4
|
+
added in inventree/InvenTree#12698, merged to InvenTree's master branch but
|
|
5
|
+
not yet present in a "stable" release. InvenTreeMCP must still import and
|
|
6
|
+
load cleanly on older InvenTree instances, so this falls back to a blank
|
|
7
|
+
mixin that simply advertises nothing when the real one isn't available.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from plugin.mixins import WellKnownMixin
|
|
12
|
+
except ImportError: # pragma: no cover - only hit on InvenTree < #12698
|
|
13
|
+
|
|
14
|
+
class WellKnownMixin:
|
|
15
|
+
"""Blank fallback used when the running InvenTree doesn't provide WellKnownMixin."""
|
|
16
|
+
|
|
17
|
+
def get_well_known_urls(self, request=None):
|
|
18
|
+
"""No well-known entries to advertise without native support."""
|
|
19
|
+
return []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
__all__ = ["WellKnownMixin"]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Propagate the authenticated Django user (and OAuth2 auth, if any) into MCP tool execution.
|
|
2
|
+
|
|
3
|
+
Tool functions are invoked by the MCP SDK's dispatcher, which does not know
|
|
4
|
+
about Django requests. The transport view sets the current identity via a
|
|
5
|
+
contextvar before handing the request off to the MCP session manager, so
|
|
6
|
+
tools (and the proxy layer) can recover *who* is calling without threading a
|
|
7
|
+
request object through every tool signature.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from contextvars import ContextVar, Token
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from django.contrib.auth.models import AbstractUser
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class _Identity:
|
|
22
|
+
user: AbstractUser
|
|
23
|
+
# The real oauth2_provider AccessToken, if the MCP request itself was
|
|
24
|
+
# OAuth2-authenticated - None for token/basic/session auth. Propagated so
|
|
25
|
+
# proxy.call_view() can make the proxied view see the token's *actual*
|
|
26
|
+
# granted scopes, not just the user's role permissions. See proxy.py.
|
|
27
|
+
oauth2_token: Any | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_current_identity: ContextVar[_Identity | None] = ContextVar(
|
|
31
|
+
"inventree_mcp_current_identity", default=None
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def set_current_user(
|
|
36
|
+
user: AbstractUser | None, oauth2_token: Any | None = None
|
|
37
|
+
) -> Token:
|
|
38
|
+
"""Bind *user* (and, if OAuth2-authenticated, the real access token) for this context."""
|
|
39
|
+
identity = _Identity(user, oauth2_token) if user is not None else None
|
|
40
|
+
return _current_identity.set(identity)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def reset_current_user(token: Token) -> None:
|
|
44
|
+
"""Undo a prior set_current_user call."""
|
|
45
|
+
_current_identity.reset(token)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def has_bound_identity() -> bool:
|
|
49
|
+
"""Return whether set_current_user() has been called in this context at all.
|
|
50
|
+
|
|
51
|
+
True for *any* real MCP request - including one that resolved to an
|
|
52
|
+
unauthenticated (AnonymousUser) identity, e.g. REQUIRE_AUTH disabled and
|
|
53
|
+
no credentials were sent. False only outside a real request (e.g. static
|
|
54
|
+
introspection - see has_current_user()'s docstring). Distinct from
|
|
55
|
+
has_current_user(): that answers "is there someone to check permissions
|
|
56
|
+
for", this answers "did a request happen at all" - callers that need to
|
|
57
|
+
tell "no request" (safe to skip filtering) apart from "a request with no
|
|
58
|
+
authenticated user" (should filter down to nothing, not everything) need
|
|
59
|
+
this one, not has_current_user().
|
|
60
|
+
"""
|
|
61
|
+
return _current_identity.get() is not None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def has_current_user() -> bool:
|
|
65
|
+
"""Return whether an *authenticated* user is currently bound.
|
|
66
|
+
|
|
67
|
+
False both outside a real MCP request (see has_bound_identity() to tell
|
|
68
|
+
that case apart) and for a real request that resolved to an
|
|
69
|
+
unauthenticated (AnonymousUser) identity - callers that only need "is
|
|
70
|
+
there someone to check permissions for" (e.g. tool_logging.py's caller
|
|
71
|
+
label) can treat both the same way; callers that need to react
|
|
72
|
+
differently to the two (e.g. tool_visibility.py, which must not show
|
|
73
|
+
every tool just because this request happened to be unauthenticated)
|
|
74
|
+
should check has_bound_identity() first.
|
|
75
|
+
"""
|
|
76
|
+
identity = _current_identity.get()
|
|
77
|
+
return identity is not None and identity.user.is_authenticated
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_current_user() -> AbstractUser:
|
|
81
|
+
"""Return the user bound to the current MCP request.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
PermissionError: if no user is bound (e.g. a tool is invoked outside
|
|
85
|
+
of a real MCP request, or authentication was not enforced).
|
|
86
|
+
"""
|
|
87
|
+
identity = _current_identity.get()
|
|
88
|
+
|
|
89
|
+
if identity is None or not identity.user.is_authenticated:
|
|
90
|
+
raise PermissionError("No authenticated user for this MCP request")
|
|
91
|
+
|
|
92
|
+
return identity.user
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_current_oauth2_token() -> Any | None:
|
|
96
|
+
"""Return the real OAuth2 access token for the current MCP request, if any."""
|
|
97
|
+
identity = _current_identity.get()
|
|
98
|
+
return identity.oauth2_token if identity else None
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""MCP server for InvenTree"""
|
|
2
|
+
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
|
|
5
|
+
from django.urls import reverse_lazy
|
|
6
|
+
from plugin import InvenTreePlugin
|
|
7
|
+
from plugin.mixins import SettingsMixin, UrlsMixin
|
|
8
|
+
|
|
9
|
+
from . import PLUGIN_VERSION
|
|
10
|
+
from ._well_known_compat import WellKnownMixin
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InvenTreeMCP(WellKnownMixin, SettingsMixin, UrlsMixin, InvenTreePlugin):
|
|
14
|
+
"""InvenTreeMCP - custom InvenTree plugin."""
|
|
15
|
+
|
|
16
|
+
# Plugin metadata
|
|
17
|
+
TITLE = "InvenTree MCP"
|
|
18
|
+
NAME = "InvenTreeMCP"
|
|
19
|
+
SLUG = "inventree-mcp"
|
|
20
|
+
DESCRIPTION = "MCP server for InvenTree"
|
|
21
|
+
VERSION = PLUGIN_VERSION
|
|
22
|
+
|
|
23
|
+
# Additional project information
|
|
24
|
+
AUTHOR = "Oliver Walters"
|
|
25
|
+
WEBSITE = "https://github.com/inventree/inventree-mcp"
|
|
26
|
+
LICENSE = "MIT"
|
|
27
|
+
|
|
28
|
+
# Supported InvenTree versions
|
|
29
|
+
MIN_VERSION = "1.5.0"
|
|
30
|
+
|
|
31
|
+
# Plugin settings (from SettingsMixin)
|
|
32
|
+
# Ref: https://docs.inventree.org/en/latest/plugins/mixins/settings/
|
|
33
|
+
SETTINGS: ClassVar[dict] = {
|
|
34
|
+
"REQUIRE_AUTH": {
|
|
35
|
+
"name": "Require Authentication",
|
|
36
|
+
"description": "Reject unauthenticated requests to the MCP endpoint. Disable only for local testing.",
|
|
37
|
+
"validator": bool,
|
|
38
|
+
"default": True,
|
|
39
|
+
},
|
|
40
|
+
"MCP_READ_ONLY": {
|
|
41
|
+
"name": "Read Only",
|
|
42
|
+
"description": "Block all write actions via the MCP endpoint, regardless of the calling user's permissions.",
|
|
43
|
+
"validator": bool,
|
|
44
|
+
"default": True,
|
|
45
|
+
},
|
|
46
|
+
"MCP_LOG_TOOL_CALLS": {
|
|
47
|
+
"name": "Log Tool Calls",
|
|
48
|
+
"description": "Log every MCP tool call (tool name, arguments, calling user, and outcome) to the 'inventree' logger for debugging.",
|
|
49
|
+
"validator": bool,
|
|
50
|
+
"default": False,
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Custom URL endpoints (from UrlsMixin)
|
|
55
|
+
# Ref: https://docs.inventree.org/en/latest/plugins/mixins/urls/
|
|
56
|
+
def setup_urls(self):
|
|
57
|
+
"""Configure custom URL endpoints for this plugin."""
|
|
58
|
+
from .mcp_transport import urlpatterns as mcp_urlpatterns
|
|
59
|
+
from .server_card import urlpatterns as server_card_urlpatterns
|
|
60
|
+
|
|
61
|
+
return mcp_urlpatterns + server_card_urlpatterns
|
|
62
|
+
|
|
63
|
+
# Well-known URLs (from WellKnownMixin, if the running InvenTree provides it)
|
|
64
|
+
# Ref: https://github.com/inventree/inventree-mcp/issues/33
|
|
65
|
+
def get_well_known_urls(self, request=None):
|
|
66
|
+
"""Advertise this plugin's MCP Server Card under /.well-known/."""
|
|
67
|
+
return [("mcp-server-card", reverse_lazy(f"plugin:{self.slug}:server-card"))]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Describe a view's optional output-expansion flags (e.g. `part_detail`).
|
|
2
|
+
|
|
3
|
+
Many InvenTree list/detail views can inline a related object's full detail
|
|
4
|
+
into the response on request (`?part_detail=true` on the stock API embeds
|
|
5
|
+
the full Part, avoiding a separate get_part call) rather than only exposing
|
|
6
|
+
its ID. These flags are declared once per view as `output_options`, an
|
|
7
|
+
`InvenTree.fields.OutputConfiguration` subclass listing `InvenTreeOutputOption`
|
|
8
|
+
instances (flag name, default, description) - the same metadata InvenTree's
|
|
9
|
+
own OpenAPI schema is generated from (see `InvenTree.schema.
|
|
10
|
+
schema_for_view_output_options`). Reading it here means this can't drift
|
|
11
|
+
from the real API the way a hand-maintained list per resource would.
|
|
12
|
+
|
|
13
|
+
Not every view declares `output_options` (flat resources with no
|
|
14
|
+
expandable relations, e.g. Company, Attachment, don't need it) -
|
|
15
|
+
describe_output_options() returns an empty dict for those rather than
|
|
16
|
+
raising, same permissive spirit as filter_introspection.py.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def describe_output_options(view_cls: type) -> dict[str, Any]:
|
|
25
|
+
"""Describe the optional output-expansion flags available on *view_cls*.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
view_cls: a list or detail view class as used in the real API
|
|
29
|
+
urlconf (e.g. stock.api.StockList or stock.api.StockDetail).
|
|
30
|
+
Both a resource's list and detail view conventionally share the
|
|
31
|
+
same `output_options` class, so either works.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
A dict of {flag_name: {default_included, description}} - each key
|
|
35
|
+
is a boolean query parameter that can be passed directly in a
|
|
36
|
+
list/get tool's `filters` argument, e.g. filters={"part_detail": true}.
|
|
37
|
+
Empty if the view declares no `output_options`.
|
|
38
|
+
"""
|
|
39
|
+
output_config = getattr(view_cls, "output_options", None)
|
|
40
|
+
if output_config is None:
|
|
41
|
+
return {}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
option.flag: {
|
|
45
|
+
"default_included": bool(option.default),
|
|
46
|
+
"description": option.description,
|
|
47
|
+
}
|
|
48
|
+
for option in output_config.OPTIONS
|
|
49
|
+
}
|