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.
Files changed (48) hide show
  1. inventree_mcp-0.3.0/LICENSE +21 -0
  2. inventree_mcp-0.3.0/MANIFEST.in +1 -0
  3. inventree_mcp-0.3.0/PKG-INFO +140 -0
  4. inventree_mcp-0.3.0/README.md +121 -0
  5. inventree_mcp-0.3.0/inventree_mcp/__init__.py +1 -0
  6. inventree_mcp-0.3.0/inventree_mcp/_well_known_compat.py +22 -0
  7. inventree_mcp-0.3.0/inventree_mcp/context.py +98 -0
  8. inventree_mcp-0.3.0/inventree_mcp/core.py +67 -0
  9. inventree_mcp-0.3.0/inventree_mcp/expand_introspection.py +49 -0
  10. inventree_mcp-0.3.0/inventree_mcp/filter_introspection.py +189 -0
  11. inventree_mcp-0.3.0/inventree_mcp/mcp_server.py +73 -0
  12. inventree_mcp-0.3.0/inventree_mcp/mcp_transport.py +222 -0
  13. inventree_mcp-0.3.0/inventree_mcp/oauth2_bridge.py +116 -0
  14. inventree_mcp-0.3.0/inventree_mcp/output_schemas.py +148 -0
  15. inventree_mcp-0.3.0/inventree_mcp/proxy.py +283 -0
  16. inventree_mcp-0.3.0/inventree_mcp/schema_introspection.py +163 -0
  17. inventree_mcp-0.3.0/inventree_mcp/server_card.py +68 -0
  18. inventree_mcp-0.3.0/inventree_mcp/settings.py +36 -0
  19. inventree_mcp-0.3.0/inventree_mcp/test_mcp.py +2491 -0
  20. inventree_mcp-0.3.0/inventree_mcp/tool_logging.py +136 -0
  21. inventree_mcp-0.3.0/inventree_mcp/tool_visibility.py +196 -0
  22. inventree_mcp-0.3.0/inventree_mcp/tools/__init__.py +5 -0
  23. inventree_mcp-0.3.0/inventree_mcp/tools/_common.py +48 -0
  24. inventree_mcp-0.3.0/inventree_mcp/tools/attachments.py +119 -0
  25. inventree_mcp-0.3.0/inventree_mcp/tools/bom.py +211 -0
  26. inventree_mcp-0.3.0/inventree_mcp/tools/build_orders.py +264 -0
  27. inventree_mcp-0.3.0/inventree_mcp/tools/categories.py +90 -0
  28. inventree_mcp-0.3.0/inventree_mcp/tools/companies.py +245 -0
  29. inventree_mcp-0.3.0/inventree_mcp/tools/discovery.py +358 -0
  30. inventree_mcp-0.3.0/inventree_mcp/tools/locations.py +90 -0
  31. inventree_mcp-0.3.0/inventree_mcp/tools/parameters.py +206 -0
  32. inventree_mcp-0.3.0/inventree_mcp/tools/parts.py +95 -0
  33. inventree_mcp-0.3.0/inventree_mcp/tools/project_codes.py +94 -0
  34. inventree_mcp-0.3.0/inventree_mcp/tools/purchase_orders.py +193 -0
  35. inventree_mcp-0.3.0/inventree_mcp/tools/return_orders.py +192 -0
  36. inventree_mcp-0.3.0/inventree_mcp/tools/sales_orders.py +286 -0
  37. inventree_mcp-0.3.0/inventree_mcp/tools/stock.py +99 -0
  38. inventree_mcp-0.3.0/inventree_mcp/tools/stock_history.py +202 -0
  39. inventree_mcp-0.3.0/inventree_mcp/tools/supplier_parts.py +184 -0
  40. inventree_mcp-0.3.0/inventree_mcp/view_resolution.py +119 -0
  41. inventree_mcp-0.3.0/inventree_mcp.egg-info/PKG-INFO +140 -0
  42. inventree_mcp-0.3.0/inventree_mcp.egg-info/SOURCES.txt +47 -0
  43. inventree_mcp-0.3.0/inventree_mcp.egg-info/dependency_links.txt +1 -0
  44. inventree_mcp-0.3.0/inventree_mcp.egg-info/entry_points.txt +2 -0
  45. inventree_mcp-0.3.0/inventree_mcp.egg-info/requires.txt +4 -0
  46. inventree_mcp-0.3.0/inventree_mcp.egg-info/top_level.txt +2 -0
  47. inventree_mcp-0.3.0/pyproject.toml +61 -0
  48. 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
+ [![PyPI](https://img.shields.io/pypi/v/inventree-mcp)](https://pypi.org/project/inventree-mcp/)
23
+ [![CI](https://github.com/inventree/inventree-mcp/actions/workflows/ci.yaml/badge.svg)](https://github.com/inventree/inventree-mcp/actions/workflows/ci.yaml)
24
+ [![codecov](https://codecov.io/gh/inventree/inventree-mcp/graph/badge.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/inventree-mcp)](https://pypi.org/project/inventree-mcp/)
4
+ [![CI](https://github.com/inventree/inventree-mcp/actions/workflows/ci.yaml/badge.svg)](https://github.com/inventree/inventree-mcp/actions/workflows/ci.yaml)
5
+ [![codecov](https://codecov.io/gh/inventree/inventree-mcp/graph/badge.svg)](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
+ }