mcp-to-cli-kit 0.1.0__tar.gz → 0.2.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.
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/PKG-INFO +63 -1
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/README.md +62 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/src/mcp_to_cli_kit/cache.py +19 -4
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/src/mcp_to_cli_kit/cli.py +212 -45
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/src/mcp_to_cli_kit/client.py +44 -16
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/src/mcp_to_cli_kit/config.py +258 -16
- mcp_to_cli_kit-0.2.0/src/mcp_to_cli_kit/config_view.py +100 -0
- mcp_to_cli_kit-0.2.0/src/mcp_to_cli_kit/redaction.py +83 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/tests/conftest.py +9 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/tests/fixture_server.py +15 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/tests/test_cli.py +30 -0
- mcp_to_cli_kit-0.2.0/tests/test_config_command.py +290 -0
- mcp_to_cli_kit-0.2.0/tests/test_expansion.py +520 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/tests/test_mounts.py +1 -0
- mcp_to_cli_kit-0.2.0/tests/test_redaction.py +49 -0
- mcp_to_cli_kit-0.2.0/tests/test_strip_prefixes.py +379 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/tests/test_tool_groups.py +1 -1
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/.editorconfig +0 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/.github/workflows/ci.yml +0 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/.github/workflows/publish.yml +0 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/.gitignore +0 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/.python-version +0 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/LICENSE +0 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/pyproject.toml +0 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/src/mcp_to_cli_kit/__init__.py +0 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/src/mcp_to_cli_kit/agent.py +0 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/src/mcp_to_cli_kit/arguments.py +0 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/src/mcp_to_cli_kit/output.py +0 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/src/mcp_to_cli_kit/py.typed +0 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/tests/test_agent.py +0 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/tests/test_arguments.py +0 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/tests/test_client.py +0 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/tests/test_config.py +0 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/tests/test_output.py +0 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/tests/test_schema.py +0 -0
- {mcp_to_cli_kit-0.1.0 → mcp_to_cli_kit-0.2.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mcp-to-cli-kit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Build a typed command-line interface for one or more remote MCP servers
|
|
5
5
|
Project-URL: Homepage, https://github.com/fprochazka/mcp-to-cli-kit
|
|
6
6
|
Project-URL: Repository, https://github.com/fprochazka/mcp-to-cli-kit
|
|
@@ -149,6 +149,27 @@ servers:
|
|
|
149
149
|
sse_read_timeout: 100 # seconds to wait for an answer, streamed or not
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
+
### Environment variables in values
|
|
153
|
+
|
|
154
|
+
The `url` and the `headers` values may refer to environment variables as `${NAME}`, so a token can stay out of the config file and the code default:
|
|
155
|
+
|
|
156
|
+
```yaml
|
|
157
|
+
servers:
|
|
158
|
+
example:
|
|
159
|
+
url: https://${EXAMPLE_HOST}/mcp
|
|
160
|
+
headers:
|
|
161
|
+
Authorization: "Bearer ${EXAMPLE_TOKEN}"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- References are expanded when the CLI contacts the server, never when the config is loaded. An unset variable therefore fails only a command that needs that server, with a config error (exit code 2) that names the variable and the server, and never shows a value. An empty variable counts as set.
|
|
165
|
+
- `--help`, a tool's `--help` and `schema` still work offline from a fresh tool cache when the variable is unset. With an expired cache the CLI warns and uses it, as when the server is down.
|
|
166
|
+
- A template can come from any layer: the code default, the config file, or an environment variable such as `EXAMPLE_TOOL_SERVERS__EXAMPLE__HEADERS__AUTHORIZATION='Bearer ${EXAMPLE_TOKEN}'`.
|
|
167
|
+
- `$${` is a literal `${`. A `$` not followed by `{` stays as it is. A `${` without a closing `}`, or one that does not enclose a variable name (letters, digits and `_`), is a config error when the config is loaded.
|
|
168
|
+
- Only `url` and the header values are expanded. Names (servers, mounts, tool groups, tools) and help texts are taken as written.
|
|
169
|
+
- A URL with references is checked once it is expanded: it must be a valid http(s) URL, and no value may move text into another part of the URL (a `/`, `?`, `#` or `@` in a password, for example), so percent-encode such characters in the variable. An expanded header value must not hold control or non-ASCII characters. Each of these is a config error that names the variable, never its value.
|
|
170
|
+
- Error messages show the URL as configured, with `${NAME}` unexpanded and without its userinfo and query. The parts that carry secrets (the values of the variables in the headers, and the userinfo, password, query and fragment of the expanded URL, also in their percent-encoded forms, if they are 4 characters or longer) are also replaced in transport errors and in the `-v` / `-vv` log. Host, port and path values are not secrets and are shown.
|
|
171
|
+
- The tool cache remembers a hash of the URL as configured and one of the expanded URL. Pointing a variable at another server fetches that server's tools; with the variable unset, the cache of the configured URL is used.
|
|
172
|
+
|
|
152
173
|
### Several servers in one bin
|
|
153
174
|
|
|
154
175
|
A bin can serve several MCP servers. Each server says where its tools appear with `mount`:
|
|
@@ -169,6 +190,22 @@ Each server keeps its own session, cache file (`<cache>/<bin>/<server>.json`), h
|
|
|
169
190
|
|
|
170
191
|
Two servers must not produce the same command name. When the tool lists show that they do (two root servers with a tool of the same name, or a group named like a top-level tool), the CLI stops with a config error that names both servers; give one of them a group or prefix mount, or hide the tool with `hidden_tools`. Calling a group checks the other servers only from what is already known (their caches), so a clash with a server whose tools were never fetched surfaces in the top-level `--help`. When a command resolves on one server while another server that could also define it cannot be reached, the CLI runs it and warns that the other server could not be checked.
|
|
171
192
|
|
|
193
|
+
### Stripping prefixes from tool names
|
|
194
|
+
|
|
195
|
+
Some servers put their own name in front of every tool (`vendor-x_list`, `vendor-x_get`). `strip_prefixes` removes it from the command names:
|
|
196
|
+
|
|
197
|
+
```yaml
|
|
198
|
+
servers:
|
|
199
|
+
example:
|
|
200
|
+
url: https://mcp.example.com/mcp
|
|
201
|
+
strip_prefixes: [vendor-x_, vendor_x-] # vendor-x_list -> example-tool list
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
- The first prefix in the list that a tool name starts with is stripped. A name that equals the prefix is kept.
|
|
205
|
+
- It works in every mount mode and happens before tool groups and mounts: `tool_groups` prefixes match the stripped names, and a group or prefix mount applies to the stripped command (`prefix: "v-"` gives `example-tool v-list`).
|
|
206
|
+
- The tool's own name stays an alias (`example-tool vendor-x_list`), and `hidden_tools` still lists tools by their own names.
|
|
207
|
+
- When a stripped name would clash with another tool or a tool group of the same server (`vendor-x_list` next to a plain `list`), that tool keeps its full name and the CLI warns on stderr, so neither tool hides the other; hide one of them with `hidden_tools` to silence it. A clash with another server's tool is a config error, as for any other command name.
|
|
208
|
+
|
|
172
209
|
### Tool groups and hidden tools
|
|
173
210
|
|
|
174
211
|
A server with many tools can split them into subcommands by tool-name prefix:
|
|
@@ -210,6 +247,31 @@ A property named like a built-in option (`help`, `input-json`, `arg-file`, `json
|
|
|
210
247
|
|
|
211
248
|
`schema` is a built-in command name: a tool whose command would be `schema` is renamed to `schema-tool`, and no group mount or tool group may be called `schema`.
|
|
212
249
|
|
|
250
|
+
### The config command
|
|
251
|
+
|
|
252
|
+
`example-tool config` prints the effective configuration as JSON: the code default, the config file and the environment variables merged, with every default filled in. Next to it, it shows the config file that was looked for (`config_file`), whether it was loaded (`config_file_loaded`), the environment variable prefix (`env_prefix`) and the names of the environment variables that were applied (`env_applied`), never their values.
|
|
253
|
+
|
|
254
|
+
```console
|
|
255
|
+
$ example-tool config --pretty
|
|
256
|
+
{
|
|
257
|
+
"config_file": "/home/me/.config/example-tool/config.yaml",
|
|
258
|
+
"config_file_loaded": true,
|
|
259
|
+
"env_prefix": "EXAMPLE_TOOL_",
|
|
260
|
+
"env_applied": ["EXAMPLE_TOOL_SERVERS__EXAMPLE__TIMEOUT"],
|
|
261
|
+
"config": {"help": null, "servers": {"example": {"url": "https://mcp.example.com/mcp", "headers": {"Authorization": "Bearer ${EXAMPLE_TOKEN}"}, ...}}}
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
- It reads the configuration only. It loads no tool list, sends nothing to any server and needs no cache, so it works offline and with unset variables.
|
|
266
|
+
- `${NAME}` references are shown as written, never expanded.
|
|
267
|
+
- Literal text that may be a secret is masked as `***` plus its last 4 characters, or only `***` when it is shorter than 12 characters. These are masked:
|
|
268
|
+
- every header value, except `Accept`, `Content-Type`, `User-Agent` and `X-Mcp-Client` (matched in any case), which are shown as written. A leading `Bearer`, `Basic`, `Token`, `Digest`, `Bot` or `ApiKey` followed by more text stays readable, as in `Bearer ***abcd`;
|
|
269
|
+
- a URL's whole userinfo (user and password), its query values and its fragment.
|
|
270
|
+
- Shown as written: the URL's scheme, host, port and path, every other setting (timeouts, mounts, tool groups, hidden tools, prefixes, help texts), and all `${NAME}` references.
|
|
271
|
+
- The output follows the usual modes: compact under an AI agent or with `--json`, indented otherwise or with `--pretty`.
|
|
272
|
+
|
|
273
|
+
`config` is a reserved command name, like `schema`: a tool whose command would be `config` is renamed to `config-tool`, and no group mount or tool group may be called `config`.
|
|
274
|
+
|
|
213
275
|
### Output and exit codes
|
|
214
276
|
|
|
215
277
|
Under an AI coding agent (detected from its environment variables), or with `--json`, the result is printed exactly as the server sent it. Otherwise, or with `--pretty`, JSON is indented with `indent=2` and non-ASCII text stays readable. Several content items become one JSON array. Nothing is ever wrapped to the terminal width.
|
|
@@ -117,6 +117,27 @@ servers:
|
|
|
117
117
|
sse_read_timeout: 100 # seconds to wait for an answer, streamed or not
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
### Environment variables in values
|
|
121
|
+
|
|
122
|
+
The `url` and the `headers` values may refer to environment variables as `${NAME}`, so a token can stay out of the config file and the code default:
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
servers:
|
|
126
|
+
example:
|
|
127
|
+
url: https://${EXAMPLE_HOST}/mcp
|
|
128
|
+
headers:
|
|
129
|
+
Authorization: "Bearer ${EXAMPLE_TOKEN}"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- References are expanded when the CLI contacts the server, never when the config is loaded. An unset variable therefore fails only a command that needs that server, with a config error (exit code 2) that names the variable and the server, and never shows a value. An empty variable counts as set.
|
|
133
|
+
- `--help`, a tool's `--help` and `schema` still work offline from a fresh tool cache when the variable is unset. With an expired cache the CLI warns and uses it, as when the server is down.
|
|
134
|
+
- A template can come from any layer: the code default, the config file, or an environment variable such as `EXAMPLE_TOOL_SERVERS__EXAMPLE__HEADERS__AUTHORIZATION='Bearer ${EXAMPLE_TOKEN}'`.
|
|
135
|
+
- `$${` is a literal `${`. A `$` not followed by `{` stays as it is. A `${` without a closing `}`, or one that does not enclose a variable name (letters, digits and `_`), is a config error when the config is loaded.
|
|
136
|
+
- Only `url` and the header values are expanded. Names (servers, mounts, tool groups, tools) and help texts are taken as written.
|
|
137
|
+
- A URL with references is checked once it is expanded: it must be a valid http(s) URL, and no value may move text into another part of the URL (a `/`, `?`, `#` or `@` in a password, for example), so percent-encode such characters in the variable. An expanded header value must not hold control or non-ASCII characters. Each of these is a config error that names the variable, never its value.
|
|
138
|
+
- Error messages show the URL as configured, with `${NAME}` unexpanded and without its userinfo and query. The parts that carry secrets (the values of the variables in the headers, and the userinfo, password, query and fragment of the expanded URL, also in their percent-encoded forms, if they are 4 characters or longer) are also replaced in transport errors and in the `-v` / `-vv` log. Host, port and path values are not secrets and are shown.
|
|
139
|
+
- The tool cache remembers a hash of the URL as configured and one of the expanded URL. Pointing a variable at another server fetches that server's tools; with the variable unset, the cache of the configured URL is used.
|
|
140
|
+
|
|
120
141
|
### Several servers in one bin
|
|
121
142
|
|
|
122
143
|
A bin can serve several MCP servers. Each server says where its tools appear with `mount`:
|
|
@@ -137,6 +158,22 @@ Each server keeps its own session, cache file (`<cache>/<bin>/<server>.json`), h
|
|
|
137
158
|
|
|
138
159
|
Two servers must not produce the same command name. When the tool lists show that they do (two root servers with a tool of the same name, or a group named like a top-level tool), the CLI stops with a config error that names both servers; give one of them a group or prefix mount, or hide the tool with `hidden_tools`. Calling a group checks the other servers only from what is already known (their caches), so a clash with a server whose tools were never fetched surfaces in the top-level `--help`. When a command resolves on one server while another server that could also define it cannot be reached, the CLI runs it and warns that the other server could not be checked.
|
|
139
160
|
|
|
161
|
+
### Stripping prefixes from tool names
|
|
162
|
+
|
|
163
|
+
Some servers put their own name in front of every tool (`vendor-x_list`, `vendor-x_get`). `strip_prefixes` removes it from the command names:
|
|
164
|
+
|
|
165
|
+
```yaml
|
|
166
|
+
servers:
|
|
167
|
+
example:
|
|
168
|
+
url: https://mcp.example.com/mcp
|
|
169
|
+
strip_prefixes: [vendor-x_, vendor_x-] # vendor-x_list -> example-tool list
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- The first prefix in the list that a tool name starts with is stripped. A name that equals the prefix is kept.
|
|
173
|
+
- It works in every mount mode and happens before tool groups and mounts: `tool_groups` prefixes match the stripped names, and a group or prefix mount applies to the stripped command (`prefix: "v-"` gives `example-tool v-list`).
|
|
174
|
+
- The tool's own name stays an alias (`example-tool vendor-x_list`), and `hidden_tools` still lists tools by their own names.
|
|
175
|
+
- When a stripped name would clash with another tool or a tool group of the same server (`vendor-x_list` next to a plain `list`), that tool keeps its full name and the CLI warns on stderr, so neither tool hides the other; hide one of them with `hidden_tools` to silence it. A clash with another server's tool is a config error, as for any other command name.
|
|
176
|
+
|
|
140
177
|
### Tool groups and hidden tools
|
|
141
178
|
|
|
142
179
|
A server with many tools can split them into subcommands by tool-name prefix:
|
|
@@ -178,6 +215,31 @@ A property named like a built-in option (`help`, `input-json`, `arg-file`, `json
|
|
|
178
215
|
|
|
179
216
|
`schema` is a built-in command name: a tool whose command would be `schema` is renamed to `schema-tool`, and no group mount or tool group may be called `schema`.
|
|
180
217
|
|
|
218
|
+
### The config command
|
|
219
|
+
|
|
220
|
+
`example-tool config` prints the effective configuration as JSON: the code default, the config file and the environment variables merged, with every default filled in. Next to it, it shows the config file that was looked for (`config_file`), whether it was loaded (`config_file_loaded`), the environment variable prefix (`env_prefix`) and the names of the environment variables that were applied (`env_applied`), never their values.
|
|
221
|
+
|
|
222
|
+
```console
|
|
223
|
+
$ example-tool config --pretty
|
|
224
|
+
{
|
|
225
|
+
"config_file": "/home/me/.config/example-tool/config.yaml",
|
|
226
|
+
"config_file_loaded": true,
|
|
227
|
+
"env_prefix": "EXAMPLE_TOOL_",
|
|
228
|
+
"env_applied": ["EXAMPLE_TOOL_SERVERS__EXAMPLE__TIMEOUT"],
|
|
229
|
+
"config": {"help": null, "servers": {"example": {"url": "https://mcp.example.com/mcp", "headers": {"Authorization": "Bearer ${EXAMPLE_TOKEN}"}, ...}}}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
- It reads the configuration only. It loads no tool list, sends nothing to any server and needs no cache, so it works offline and with unset variables.
|
|
234
|
+
- `${NAME}` references are shown as written, never expanded.
|
|
235
|
+
- Literal text that may be a secret is masked as `***` plus its last 4 characters, or only `***` when it is shorter than 12 characters. These are masked:
|
|
236
|
+
- every header value, except `Accept`, `Content-Type`, `User-Agent` and `X-Mcp-Client` (matched in any case), which are shown as written. A leading `Bearer`, `Basic`, `Token`, `Digest`, `Bot` or `ApiKey` followed by more text stays readable, as in `Bearer ***abcd`;
|
|
237
|
+
- a URL's whole userinfo (user and password), its query values and its fragment.
|
|
238
|
+
- Shown as written: the URL's scheme, host, port and path, every other setting (timeouts, mounts, tool groups, hidden tools, prefixes, help texts), and all `${NAME}` references.
|
|
239
|
+
- The output follows the usual modes: compact under an AI agent or with `--json`, indented otherwise or with `--pretty`.
|
|
240
|
+
|
|
241
|
+
`config` is a reserved command name, like `schema`: a tool whose command would be `config` is renamed to `config-tool`, and no group mount or tool group may be called `config`.
|
|
242
|
+
|
|
181
243
|
### Output and exit codes
|
|
182
244
|
|
|
183
245
|
Under an AI coding agent (detected from its environment variables), or with `--json`, the result is printed exactly as the server sent it. Otherwise, or with `--pretty`, JSON is indented with `indent=2` and non-ASCII text stays readable. Several content items become one JSON array. Nothing is ever wrapped to the terminal width.
|
|
@@ -6,6 +6,11 @@ A cache entry is fresh for an hour. An expired entry is still kept: when the ser
|
|
|
6
6
|
be reached, the CLI falls back to it rather than failing to know its own commands. The entry
|
|
7
7
|
records a hash of the server URL, never the URL itself, which may carry a token; an entry
|
|
8
8
|
written for another URL is ignored.
|
|
9
|
+
|
|
10
|
+
A configured URL may be a template with ``${NAME}`` references. The entry then records a hash
|
|
11
|
+
of the template and one of the expanded URL. When the variables are set, both must match, so
|
|
12
|
+
pointing a variable at another server does not reuse the old server's tools; when one is not
|
|
13
|
+
set, the template alone decides, so ``--help`` still lists the tools offline.
|
|
9
14
|
"""
|
|
10
15
|
|
|
11
16
|
from __future__ import annotations
|
|
@@ -60,8 +65,14 @@ class SchemaCache:
|
|
|
60
65
|
"""The cache file of one server."""
|
|
61
66
|
return self.directory / f"{server}.json"
|
|
62
67
|
|
|
63
|
-
def load(self, server: str, url: str) -> CachedTools | None:
|
|
64
|
-
"""Return the cached tools of a server, fresh or expired, or None when there is no usable entry.
|
|
68
|
+
def load(self, server: str, url: str, resolved_url: str | None = None) -> CachedTools | None:
|
|
69
|
+
"""Return the cached tools of a server, fresh or expired, or None when there is no usable entry.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
server: The server's name.
|
|
73
|
+
url: The URL as configured, a template or not.
|
|
74
|
+
resolved_url: The URL with its variables expanded, or None when they cannot be.
|
|
75
|
+
"""
|
|
65
76
|
path = self.path(server)
|
|
66
77
|
try:
|
|
67
78
|
content = json.loads(path.read_text(encoding="utf-8"))
|
|
@@ -75,6 +86,7 @@ class SchemaCache:
|
|
|
75
86
|
not isinstance(content, dict)
|
|
76
87
|
or content.get("version") != _FORMAT_VERSION
|
|
77
88
|
or content.get("url_sha256") != _url_hash(url)
|
|
89
|
+
or (resolved_url not in (None, url) and content.get("resolved_url_sha256") != _url_hash(resolved_url))
|
|
78
90
|
or not isinstance(content.get("tools"), list)
|
|
79
91
|
or not isinstance(content.get("fetched_at"), int | float)
|
|
80
92
|
):
|
|
@@ -86,9 +98,12 @@ class SchemaCache:
|
|
|
86
98
|
tools = [tool for tool in content["tools"] if isinstance(tool, dict)]
|
|
87
99
|
return CachedTools(tools=tools, fetched_at=fetched_at, fresh=0 <= age < self.ttl_seconds)
|
|
88
100
|
|
|
89
|
-
def save(self, server: str, url: str, tools: list[dict[str, Any]]) -> None:
|
|
101
|
+
def save(self, server: str, url: str, tools: list[dict[str, Any]], resolved_url: str | None = None) -> None:
|
|
90
102
|
"""Write a server's tools atomically. A failure to write is logged, never raised."""
|
|
91
|
-
content = {"version": _FORMAT_VERSION, "url_sha256": _url_hash(url)
|
|
103
|
+
content: dict[str, Any] = {"version": _FORMAT_VERSION, "url_sha256": _url_hash(url)}
|
|
104
|
+
if resolved_url is not None and resolved_url != url:
|
|
105
|
+
content["resolved_url_sha256"] = _url_hash(resolved_url)
|
|
106
|
+
content.update(fetched_at=time.time(), tools=tools)
|
|
92
107
|
path = self.path(server)
|
|
93
108
|
try:
|
|
94
109
|
self.directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|