itential-mcp 0.1.0__py3-none-any.whl
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.
- itential_mcp/__init__.py +10 -0
- itential_mcp/__main__.py +9 -0
- itential_mcp/cli.py +162 -0
- itential_mcp/client.py +182 -0
- itential_mcp/config.py +37 -0
- itential_mcp/env.py +39 -0
- itential_mcp/response.py +54 -0
- itential_mcp/server.py +166 -0
- itential_mcp/stringutils.py +73 -0
- itential_mcp/tools/compliance_reports.py +32 -0
- itential_mcp/tools/devices.py +60 -0
- itential_mcp/tools/jobs.py +31 -0
- itential_mcp/tools/system.py +55 -0
- itential_mcp/tools/triggers.py +57 -0
- itential_mcp/tools/wfe.py +100 -0
- itential_mcp/tools/workflows.py +213 -0
- itential_mcp-0.1.0.dist-info/METADATA +215 -0
- itential_mcp-0.1.0.dist-info/RECORD +21 -0
- itential_mcp-0.1.0.dist-info/WHEEL +4 -0
- itential_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- itential_mcp-0.1.0.dist-info/licenses/LICENSE +674 -0
itential_mcp/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Copyright (c) 2025 Itential, Inc
|
|
2
|
+
# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
|
+
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
|
|
6
|
+
from .cli import run
|
|
7
|
+
|
|
8
|
+
__version__ = version("itential_mcp")
|
|
9
|
+
|
|
10
|
+
__all__ = ("run",)
|
itential_mcp/__main__.py
ADDED
itential_mcp/cli.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Copyright (c) 2025 Itential, Inc
|
|
2
|
+
# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import asyncio
|
|
7
|
+
import argparse
|
|
8
|
+
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from . import server
|
|
14
|
+
from . import env
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_args(args: Sequence) -> dict[str, Any]:
|
|
18
|
+
"""
|
|
19
|
+
Parses any arguments
|
|
20
|
+
|
|
21
|
+
This function will parse the arguments identified by the `args` argument
|
|
22
|
+
and return a Namespace object with the values. Typically this is used
|
|
23
|
+
to parse command line arguments passed when the application starts.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
args (Sequence): The list of arguments to parse
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
dict[str, Any]: Returns a Python dict object that has parsed the args
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
None
|
|
33
|
+
"""
|
|
34
|
+
parser = argparse.ArgumentParser(prog="itential-mcp")
|
|
35
|
+
|
|
36
|
+
# MCP Server arguments
|
|
37
|
+
server_group = parser.add_argument_group("MCP Server")
|
|
38
|
+
|
|
39
|
+
server_group.add_argument(
|
|
40
|
+
"--transport",
|
|
41
|
+
default="stdio",
|
|
42
|
+
help="The MCP server transport to use (default=stdio)"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
server_group.add_argument(
|
|
46
|
+
"--host",
|
|
47
|
+
default="localhost",
|
|
48
|
+
help="Address to listen for connections on (default=localhost)"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
server_group.add_argument(
|
|
52
|
+
"--port",
|
|
53
|
+
type=int,
|
|
54
|
+
default=8000,
|
|
55
|
+
help="Port to listen for connections on (default=8000)",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
server_group.add_argument(
|
|
59
|
+
"--log-level",
|
|
60
|
+
default="INFO",
|
|
61
|
+
help="Logging level. One of DEBUG, INFO, WARNING, ERROR, CRITICAL. (default=INFO)"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Itential Platform arguments
|
|
65
|
+
platform_group = parser.add_argument_group("Itential Platform")
|
|
66
|
+
|
|
67
|
+
platform_group.add_argument(
|
|
68
|
+
"--platform-host",
|
|
69
|
+
default="localhost",
|
|
70
|
+
help="The host address of Itential Platform to connect to (default=localhost)"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
platform_group.add_argument(
|
|
74
|
+
"--platform-port",
|
|
75
|
+
type=int,
|
|
76
|
+
default=0,
|
|
77
|
+
help="The port to use when connecting to Itential Platform (default=0)"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
platform_group.add_argument(
|
|
81
|
+
"--platform-disable-tls",
|
|
82
|
+
action="store_true",
|
|
83
|
+
help="Disable using TLS to connect to the server (default=False)"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
platform_group.add_argument(
|
|
87
|
+
"--platform-disable-verify",
|
|
88
|
+
action="store_true",
|
|
89
|
+
help="Disable certificate verification (default=False)",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
platform_group.add_argument(
|
|
93
|
+
"--platform-user",
|
|
94
|
+
default="admin",
|
|
95
|
+
help="Username to use when authenticating to the server (default=admin)"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
platform_group.add_argument(
|
|
99
|
+
"--platform-password",
|
|
100
|
+
default="admin",
|
|
101
|
+
help="Password to use when authenticating to the server (default=admin)"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
platform_group.add_argument(
|
|
105
|
+
"--platform-client-id",
|
|
106
|
+
help="Client ID to use when authenticating to the server with OAuth",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
platform_group.add_argument(
|
|
110
|
+
"--platform-client-secret",
|
|
111
|
+
help="Client secret to use when authenticating to the server with OAuth",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
platform_group.add_argument(
|
|
115
|
+
"--platform-timeout",
|
|
116
|
+
type=int,
|
|
117
|
+
default=30,
|
|
118
|
+
help="Configure the connection timeout in seconds (default=30)",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
args = parser.parse_args(args=args)
|
|
122
|
+
|
|
123
|
+
kwargs = {}
|
|
124
|
+
|
|
125
|
+
for key, value in dict(args._get_kwargs()).items():
|
|
126
|
+
envkey = f"ITENTIAL_MCP_{key}".upper()
|
|
127
|
+
|
|
128
|
+
if key.startswith("platform"):
|
|
129
|
+
if value is not None:
|
|
130
|
+
if envkey not in os.environ:
|
|
131
|
+
os.environ[envkey] = str(value)
|
|
132
|
+
else:
|
|
133
|
+
if isinstance(value, bool):
|
|
134
|
+
v = env.getbool(envkey, default=value)
|
|
135
|
+
elif isinstance(value, int):
|
|
136
|
+
v = env.getint(envkey, default=value)
|
|
137
|
+
elif isinstance(value, str):
|
|
138
|
+
v = env.getstr(envkey, default=value)
|
|
139
|
+
else:
|
|
140
|
+
v = value
|
|
141
|
+
kwargs[key] = v
|
|
142
|
+
|
|
143
|
+
return kwargs
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def run() -> int:
|
|
147
|
+
"""
|
|
148
|
+
Main entry point for the application
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
None
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
int:
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
None
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
return asyncio.run(server.run(**parse_args(sys.argv[1:])))
|
|
161
|
+
except Exception:
|
|
162
|
+
sys.exit(1)
|
itential_mcp/client.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Copyright (c) 2025 Itential, Inc
|
|
2
|
+
# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
|
+
|
|
4
|
+
import ipsdk
|
|
5
|
+
|
|
6
|
+
from ipsdk.platform import AsyncPlatform
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from . import config
|
|
11
|
+
from . import response
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PlatformClient(object):
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.client = self._init_client()
|
|
18
|
+
|
|
19
|
+
def _init_client(self) -> AsyncPlatform:
|
|
20
|
+
"""
|
|
21
|
+
Initializes the client connection to Itential Platform
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
None
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
AsyncPlatform: An instance of AsyncPlatform
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
None
|
|
31
|
+
"""
|
|
32
|
+
return ipsdk.platform_factory(want_async=True, **config.get())
|
|
33
|
+
|
|
34
|
+
async def _make_response(self, res: httpx.Response) -> response.Response:
|
|
35
|
+
"""
|
|
36
|
+
Creates a response object and returns it
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
res (httpx.Response): The response object returned from the HTTP API
|
|
40
|
+
request
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Response: A HTTP Response object
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
None
|
|
47
|
+
"""
|
|
48
|
+
return response.Response(res)
|
|
49
|
+
|
|
50
|
+
async def send_request(
|
|
51
|
+
self,
|
|
52
|
+
method: str,
|
|
53
|
+
path: str,
|
|
54
|
+
params: dict = None,
|
|
55
|
+
json: str | bytes | dict | list | None = None
|
|
56
|
+
) -> response.Response:
|
|
57
|
+
"""
|
|
58
|
+
Send the request to the server and return the response
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
method (str): The HTTP method to invoke. This should be one of
|
|
62
|
+
"GET", "POST", "PUT", "DELETE"
|
|
63
|
+
|
|
64
|
+
path (str): The full URL path to send the reques to
|
|
65
|
+
|
|
66
|
+
params (dict): A Python dict objec to be converted into a query
|
|
67
|
+
string and appeneded to the URL
|
|
68
|
+
|
|
69
|
+
json (str|bytes|dict|list): A Python object that can be serialized
|
|
70
|
+
into a JSON object.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Response: The HTTP response from the server
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
None
|
|
77
|
+
"""
|
|
78
|
+
res = await self.client._send_request(method, path, params, json)
|
|
79
|
+
return await self._make_response(res)
|
|
80
|
+
|
|
81
|
+
async def get(
|
|
82
|
+
self,
|
|
83
|
+
path: str,
|
|
84
|
+
params: dict | None = None
|
|
85
|
+
) -> response.Response:
|
|
86
|
+
"""
|
|
87
|
+
Send a HTTP GET request to the server
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
path (str): The full path to send the HTTP request o
|
|
91
|
+
|
|
92
|
+
params (dict): A Python dict object to be converted to a query
|
|
93
|
+
string and appended to the path
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Response: An HTTP Response object from the server
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
None
|
|
100
|
+
"""
|
|
101
|
+
return await self.send_request(
|
|
102
|
+
method="GET", path=path, params=params
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
async def post(
|
|
106
|
+
self,
|
|
107
|
+
path: str,
|
|
108
|
+
params: dict | None = None,
|
|
109
|
+
json: str | dict | list | None = None,
|
|
110
|
+
) -> response.Response:
|
|
111
|
+
"""
|
|
112
|
+
Send a HTTP POST request to the server
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
path (str): The full path to send the HTTP request o
|
|
116
|
+
|
|
117
|
+
params (dict): A Python dict object to be converted to a query
|
|
118
|
+
string and appended to the path
|
|
119
|
+
|
|
120
|
+
json (str | dict | list): A Python object that can be serialized
|
|
121
|
+
to a JSON string and sent as the body of the request
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Response: An HTTP Response object from the server
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
None
|
|
128
|
+
"""
|
|
129
|
+
return await self.send_request(
|
|
130
|
+
method="POST", path=path, params=params, json=json
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
async def put(
|
|
134
|
+
self,
|
|
135
|
+
path: str,
|
|
136
|
+
params: dict | None = None,
|
|
137
|
+
json: str | dict | list | None = None,
|
|
138
|
+
) -> response.Response:
|
|
139
|
+
"""
|
|
140
|
+
Send a HTTP PUT request to the server
|
|
141
|
+
Args:
|
|
142
|
+
path (str): The full path to send the HTTP request o
|
|
143
|
+
|
|
144
|
+
params (dict): A Python dict object to be converted to a query
|
|
145
|
+
string and appended to the path
|
|
146
|
+
|
|
147
|
+
json (str | dict | list): A Python object that can be serialized
|
|
148
|
+
to a JSON string and sent as the body of the request
|
|
149
|
+
Returns:
|
|
150
|
+
Response: An HTTP Response object from the server
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
None
|
|
154
|
+
"""
|
|
155
|
+
return await self.send_request(
|
|
156
|
+
method="PUT", path=path, params=params, json=json
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
async def delete(
|
|
160
|
+
self,
|
|
161
|
+
path: str,
|
|
162
|
+
params: dict | None = None,
|
|
163
|
+
) -> response.Response:
|
|
164
|
+
"""
|
|
165
|
+
Send a HTTP DELETE request to the server
|
|
166
|
+
Args:
|
|
167
|
+
path (str): The full path to send the HTTP request o
|
|
168
|
+
|
|
169
|
+
params (dict): A Python dict object to be converted to a query
|
|
170
|
+
string and appended to the path
|
|
171
|
+
|
|
172
|
+
json (str | dict | list): A Python object that can be serialized
|
|
173
|
+
to a JSON string and sent as the body of the request
|
|
174
|
+
Returns:
|
|
175
|
+
Response: An HTTP Response object from the server
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
None
|
|
179
|
+
"""
|
|
180
|
+
return await self.send_request(
|
|
181
|
+
method="DELETE", path=path, params=params
|
|
182
|
+
)
|
itential_mcp/config.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Copyright (c) 2025 Itential, Inc
|
|
2
|
+
# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
|
+
|
|
4
|
+
from . import env
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get() -> dict:
|
|
8
|
+
"""
|
|
9
|
+
Load the configuration from the environment
|
|
10
|
+
|
|
11
|
+
This function will load the Platform configuration from the environment
|
|
12
|
+
and return it as a Python dict object. The returned object can be passed
|
|
13
|
+
into the `platform_factory(...)` function to create a new instance.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
None
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
dict: Returns a Python dict object that represents the configuration
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
None
|
|
23
|
+
"""
|
|
24
|
+
return {
|
|
25
|
+
"host": env.getstr("ITENTIAL_MCP_PLATFORM_HOST", default="localhost"),
|
|
26
|
+
"port": env.getint("ITENTIAL_MCP_PLATFORM_PORT", default=0),
|
|
27
|
+
|
|
28
|
+
"use_tls": not env.getbool("ITENTIAL_MCP_PLATFORM_DISABLE_TLS", default=False),
|
|
29
|
+
"verify": not env.getbool("ITENTIAL_MCP_PLATFORM_DISABLE_VERIFY", default=False),
|
|
30
|
+
"timeout": env.getint("ITENTIAL_MCP_PLATFORM_TIMEOUT", default=30),
|
|
31
|
+
|
|
32
|
+
"user": env.getstr("ITENTIAL_MCP_PLATFORM_USER", default="admin"),
|
|
33
|
+
"password": env.getstr("ITENTIAL_MCP_PLATFORM_PASSWORD", default="admin"),
|
|
34
|
+
|
|
35
|
+
"client_id": env.getstr("ITENTIAL_MCP_PLATFORM_CLIENT_ID"),
|
|
36
|
+
"client_secret": env.getstr("ITENTIAL_MCP_PLATFORM_CLIENT_SECRET"),
|
|
37
|
+
}
|
itential_mcp/env.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Copyright (c) 2025 Itential, Inc
|
|
2
|
+
# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from typing import Callable, Any
|
|
7
|
+
|
|
8
|
+
from functools import partial
|
|
9
|
+
|
|
10
|
+
from . import stringutils
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get(f: Callable, key: str, default: Any | None=None) -> Any:
|
|
14
|
+
"""
|
|
15
|
+
Get the value for key from the current environment
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
f (Callable): The function to transform the value
|
|
19
|
+
key (str): The key to looking in the environment
|
|
20
|
+
default (Any): The default value to use if the key doesn't exist
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Any: The transformed value from the environment or the default vaule
|
|
24
|
+
if the key doesn't exist
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
None
|
|
28
|
+
"""
|
|
29
|
+
return f(os.getenv(key, default=default))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
getstr = partial(get, stringutils.tostr)
|
|
33
|
+
getstr.__doc__ = "Get a string value from the environment"
|
|
34
|
+
|
|
35
|
+
getint = partial(get, stringutils.toint)
|
|
36
|
+
getint.__doc__ = "Get a integer value from the envirnment"
|
|
37
|
+
|
|
38
|
+
getbool = partial(get, stringutils.tobool)
|
|
39
|
+
getbool.__doc__ = "Get a boolean value from the environment"
|
itential_mcp/response.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Copyright (c) 2025 Itential, Inc
|
|
2
|
+
# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Response(object):
|
|
10
|
+
|
|
11
|
+
def __init__(self, res: httpx.Response):
|
|
12
|
+
"""
|
|
13
|
+
Initialize a new response instance
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
res: An instance of httpx.Response
|
|
17
|
+
"""
|
|
18
|
+
self.response = res
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def status_code(self) -> int:
|
|
22
|
+
"""
|
|
23
|
+
Returns the HTTP status code
|
|
24
|
+
"""
|
|
25
|
+
return self.response.status_code
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def reason(self) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Returns the HTTP status code message
|
|
31
|
+
"""
|
|
32
|
+
return self.response.reason_phrase
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def text(self) -> str:
|
|
36
|
+
"""
|
|
37
|
+
Return the response body as text
|
|
38
|
+
"""
|
|
39
|
+
return self.response.text
|
|
40
|
+
|
|
41
|
+
def json(self) -> Any:
|
|
42
|
+
"""
|
|
43
|
+
Attempt to return the response body as a Python object
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
None
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Any: Returns the response text as a object
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
None
|
|
53
|
+
"""
|
|
54
|
+
return self.response.json()
|
itential_mcp/server.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Copyright (c) 2025 Itential, Inc
|
|
2
|
+
# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import inspect
|
|
7
|
+
import importlib.util
|
|
8
|
+
|
|
9
|
+
from collections.abc import AsyncGenerator
|
|
10
|
+
from contextlib import asynccontextmanager, AsyncExitStack
|
|
11
|
+
|
|
12
|
+
from typing import Any, Literal
|
|
13
|
+
|
|
14
|
+
from fastmcp import FastMCP
|
|
15
|
+
|
|
16
|
+
from . import client
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@asynccontextmanager
|
|
20
|
+
async def lifespan(mcp: FastMCP) -> AsyncGenerator[dict[str | Any], None]:
|
|
21
|
+
"""
|
|
22
|
+
Manage the lifespan of Itential Platform servers
|
|
23
|
+
|
|
24
|
+
This function is responsible for creating the client connection to
|
|
25
|
+
Itential Platform and yielding it to FastMCP to be included in the
|
|
26
|
+
context.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
mcp (FastMCP): An instance of FastMCP
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
AsyncGenerator: Yields an AsyncGenerator with a dict object
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
None
|
|
36
|
+
"""
|
|
37
|
+
async with AsyncExitStack():
|
|
38
|
+
yield {
|
|
39
|
+
"client": client.PlatformClient()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def register_tools(mcp: FastMCP) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Register all functions in the tools folder with mcp
|
|
46
|
+
|
|
47
|
+
This function will recursively load all modules found in the `tools`
|
|
48
|
+
folder as long as they module name does not start with underscore (_). It
|
|
49
|
+
will then inspect the module to find all public functions and attach
|
|
50
|
+
them to the instance of mcp as a tool.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
mcp (FastMCP): An instance of FastMCP to attach tools to
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
None
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
None
|
|
60
|
+
"""
|
|
61
|
+
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "tools")
|
|
62
|
+
|
|
63
|
+
# Get a list of all files in the directory
|
|
64
|
+
module_files = [f[:-3] for f in os.listdir(path) if f.endswith(".py") and f != "__init__.py"]
|
|
65
|
+
|
|
66
|
+
# Import the modules, add them to globals and mcp
|
|
67
|
+
for module_name in module_files:
|
|
68
|
+
if not module_name.startswith("_"):
|
|
69
|
+
spec = importlib.util.spec_from_file_location(module_name, os.path.join(path, f"{module_name}.py"))
|
|
70
|
+
module = importlib.util.module_from_spec(spec)
|
|
71
|
+
spec.loader.exec_module(module)
|
|
72
|
+
|
|
73
|
+
# Add the module to globals
|
|
74
|
+
globals()[module_name] = module
|
|
75
|
+
|
|
76
|
+
# Inspect the module to retreive all of the functions
|
|
77
|
+
for name, f in inspect.getmembers(module, inspect.isfunction):
|
|
78
|
+
if not name.startswith("_"):
|
|
79
|
+
mcp.add_tool(f)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def run(
|
|
83
|
+
transport: str = "stdio",
|
|
84
|
+
host: str | None = "localhost",
|
|
85
|
+
port: int = 8000,
|
|
86
|
+
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | int = "INFO",
|
|
87
|
+
) -> int:
|
|
88
|
+
"""
|
|
89
|
+
Run the MCP server
|
|
90
|
+
|
|
91
|
+
This function will run the MCP server using the configured transport
|
|
92
|
+
option.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
args (Sequence | None):
|
|
96
|
+
transport (str): Server transport to use. Valid values for this
|
|
97
|
+
argument are `sse` or `stdio`. The default value is `stdio`
|
|
98
|
+
|
|
99
|
+
host (str | None): The host to run the server on. The default
|
|
100
|
+
value for host is "localhost"
|
|
101
|
+
|
|
102
|
+
port (int): The port on the host to listen for connections on. The
|
|
103
|
+
default value for port is 0.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
int: Returns a int value as the return code from running the server
|
|
107
|
+
A value of 0 is success and any other value is an error
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
ValueError: When the value of transport is not a valid value
|
|
111
|
+
ValueError: When the value of log_level is not a valid value
|
|
112
|
+
"""
|
|
113
|
+
if transport not in ("stdio", "sse"):
|
|
114
|
+
raise ValueError("invalid setting for transport")
|
|
115
|
+
|
|
116
|
+
if isinstance(log_level, str):
|
|
117
|
+
if log_level.upper() not in ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"):
|
|
118
|
+
raise ValueError("invalid setting for log_level")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# Initialize FastMCP server
|
|
123
|
+
# Available keyword args:
|
|
124
|
+
# - name
|
|
125
|
+
# - instructions
|
|
126
|
+
# - lifespan
|
|
127
|
+
# - tags
|
|
128
|
+
# - host
|
|
129
|
+
# - port
|
|
130
|
+
# - log_level
|
|
131
|
+
# - on_duplicate_tools
|
|
132
|
+
# - on_duplicate_resources
|
|
133
|
+
# - on_duplicate_prompts
|
|
134
|
+
#
|
|
135
|
+
# The server settings can also be passed using env variables prefixed
|
|
136
|
+
# with FASTMCP_SERVER_
|
|
137
|
+
mcp = FastMCP(
|
|
138
|
+
name="Itential Platform MCP",
|
|
139
|
+
instructions="Itential tools and resources for interacting with Itential Platform",
|
|
140
|
+
lifespan=lifespan,
|
|
141
|
+
log_level=log_level.upper(),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
register_tools(mcp)
|
|
146
|
+
except Exception as exc:
|
|
147
|
+
print(f"ERROR: failed to import tool: {str(exc)}", file=sys.stderr)
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
|
|
150
|
+
kwargs = {
|
|
151
|
+
"transport": transport,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if transport == "sse":
|
|
155
|
+
kwargs.update({
|
|
156
|
+
"host": host,
|
|
157
|
+
"port": port,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
await mcp.run_async(**kwargs)
|
|
162
|
+
except KeyboardInterrupt:
|
|
163
|
+
sys.exit(0)
|
|
164
|
+
except Exception as exc:
|
|
165
|
+
print(f"ERROR: failed to import tool: {str(exc)}", file=sys.stderr)
|
|
166
|
+
sys.exit(1)
|