insightfactory-cli 1.1.1.dev25__tar.gz → 1.1.2__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.
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/.gitignore +1 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/CHANGELOG.md +44 -2
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/CLAUDE.md +10 -6
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/PKG-INFO +92 -14
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/README.md +91 -13
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/pyproject.toml +1 -1
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/commands/mcp.py +21 -3
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/config.py +16 -2
- insightfactory_cli-1.1.2/src/if_cli/router/content.py +298 -0
- insightfactory_cli-1.1.2/src/if_cli/router/log.py +20 -0
- insightfactory_cli-1.1.2/src/if_cli/router/server.py +594 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/router/upstream.py +113 -8
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_config.py +30 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_mcp_command.py +216 -7
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_release_tools.py +136 -8
- insightfactory_cli-1.1.2/tests/test_router_content.py +310 -0
- insightfactory_cli-1.1.2/tests/test_router_log.py +23 -0
- insightfactory_cli-1.1.2/tests/test_router_resources.py +436 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_router_upstream.py +36 -5
- insightfactory_cli-1.1.2/tools/check_release.py +163 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/uv.lock +1 -1
- insightfactory_cli-1.1.1.dev25/src/if_cli/router/log.py +0 -13
- insightfactory_cli-1.1.1.dev25/src/if_cli/router/server.py +0 -281
- insightfactory_cli-1.1.1.dev25/tools/check_release.py +0 -79
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/.github/workflows/ci.yml +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/.github/workflows/claude.yml +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/.github/workflows/release.yml +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/.python-version +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/AGENTS.md +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/LICENSE +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/__init__.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/__main__.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/assets/__init__.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/assets/insightfactoryai-logo.svg +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/cache.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/callback_page.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/cli.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/colour.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/commands/__init__.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/commands/api.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/commands/config.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/commands/login.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/commands/logout.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/commands/profiles.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/commands/set_token.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/commands/token.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/constants.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/http.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/main.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/oauth.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/router/__init__.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/router/catalog.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/router/policy.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/runtime.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/__init__.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/cache_writer.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/conftest.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/helpers.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/servers.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_api.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_api_command.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_cache.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_cli.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_config_command.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_login.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_oauth.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_oauth_flow.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_oauth_force_refresh.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_profiles.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_programmatic_api.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_router_catalog.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_router_policy.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_router_server.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_runtime.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_set_token.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_token.py +0 -0
- {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tools/release_notes.py +0 -0
|
@@ -7,8 +7,50 @@ project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
The release job reads the section whose heading matches the version in
|
|
9
9
|
`pyproject.toml` and makes it the body of the GitHub Release. CI holds both ends of
|
|
10
|
-
that:
|
|
11
|
-
|
|
10
|
+
that: every PR into `develop` must add an entry here, and a tag whose version has no
|
|
11
|
+
section fails rather than releasing an empty page. The version only moves when the
|
|
12
|
+
last one has been released, so a cycle's PRs share a section.
|
|
13
|
+
|
|
14
|
+
## 1.1.2
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- The MCP router serves resources, prompts and completion as well as tools, so a
|
|
19
|
+
factory's hosted skills reach a client through `if-cli mcp` instead of only through a
|
|
20
|
+
direct connection. `skill://` bodies come from the `--catalog` environment as
|
|
21
|
+
published; a resource template is republished with an `environment` variable added so
|
|
22
|
+
the client picks the factory when it expands the URI; a prompt gains the same required
|
|
23
|
+
`environment` argument a tool gets, plus a line naming the chosen environment in the
|
|
24
|
+
rendered text.
|
|
25
|
+
- `--skills` / `--no-skills`, and `skills = true` in a `[factory <name>]` section,
|
|
26
|
+
advertise the experimental skills extension (SEP-2640). Off by default: the handshake
|
|
27
|
+
is answered before any factory has been reached, so the claim is made only when
|
|
28
|
+
someone asks for it. The resources themselves are served either way, and only a
|
|
29
|
+
client on protocol 2026-07-28 or later can see the advertisement, since the
|
|
30
|
+
`initialize` handshake predates `capabilities.extensions`.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- The router's stderr lines now carry the elapsed time since startup, and it logs when
|
|
35
|
+
the catalogue build starts and when it answers its first `initialize`, so an operator
|
|
36
|
+
can tell a slow process apart from a slow factory and a client that called before the
|
|
37
|
+
handshake finished. `if-cli mcp` also now logs a `starting on Python <version>` line
|
|
38
|
+
before doing anything else, so a process that dies during argument parsing, config
|
|
39
|
+
load, or profile lookup still leaves one line on stderr instead of none.
|
|
40
|
+
- `release-guard` asks every PR into `develop` for a `CHANGELOG.md` entry, and asks
|
|
41
|
+
for a version bump only when `develop` is still on the version `main` is on. One
|
|
42
|
+
bump opens a release cycle and the PRs after it add to the section it opened,
|
|
43
|
+
rather than each minting a version that is never tagged.
|
|
44
|
+
- The guard now measures a version against `main` as well as the base branch. A bump
|
|
45
|
+
has to land above the released version, not merely above the base, and a PR into an
|
|
46
|
+
open cycle has to add a line rather than reword one.
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- The router no longer turns a `confirmed:false` preview from a mutating tool into an
|
|
51
|
+
error pointing at a web UI. That path only belongs to the Databricks connect flow;
|
|
52
|
+
a form-mode approval gate now comes back as an ordinary result, so the caller
|
|
53
|
+
confirms by calling again with `confirmed:true` as intended.
|
|
12
54
|
|
|
13
55
|
## 1.1.1
|
|
14
56
|
|
|
@@ -34,9 +34,13 @@ uv run ty check
|
|
|
34
34
|
match `pyproject.toml`. Pushing `develop` publishes a `.dev{run_number}`
|
|
35
35
|
pre-release. Creating the release tag is a deliberate human act — do not create
|
|
36
36
|
or push one unless asked to cut a release.
|
|
37
|
-
- **Every PR into `develop`
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
- **Every PR into `develop` adds a `CHANGELOG.md` entry**, in the Added/Changed/Fixed
|
|
38
|
+
group that fits. For internal-only work (tests, CI, a refactor nothing observes) a
|
|
39
|
+
one-line entry saying so is enough. The section becomes the body of that version's
|
|
40
|
+
GitHub Release.
|
|
41
|
+
- **Raise `version` in `pyproject.toml` only when `develop` is on the same version as
|
|
42
|
+
`main`.** That version is released, so a new entry under it would describe a change
|
|
43
|
+
it does not contain: bump, and open a section for the new version. While `develop`
|
|
44
|
+
is ahead of `main` the cycle is open and PRs add to the section already there. A PR
|
|
45
|
+
into `main` always raises the version. The `release-guard` job runs
|
|
46
|
+
`tools/check_release.py` and fails the PR otherwise.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: insightfactory-cli
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.2
|
|
4
4
|
Summary: Profile-based authentication CLI for the InsightFactory Interfaces API
|
|
5
5
|
Project-URL: Homepage, https://insightfactory.ai
|
|
6
6
|
Author-email: "insightfactory.ai Support" <support@insightfactory.ai>
|
|
@@ -255,15 +255,24 @@ tst = example-tst
|
|
|
255
255
|
writable = dev
|
|
256
256
|
```
|
|
257
257
|
|
|
258
|
-
Every key except `writable` and `
|
|
259
|
-
order is the environment order. `writable` is a comma-separated list of codes (default
|
|
260
|
-
none); `catalog` is one code (default the first environment)
|
|
258
|
+
Every key except `writable`, `catalog` and `skills` is `<environment code> = <profile name>`;
|
|
259
|
+
file order is the environment order. `writable` is a comma-separated list of codes (default
|
|
260
|
+
none); `catalog` is one code (default the first environment); `skills` is `true` or `false`
|
|
261
|
+
(default false). Then:
|
|
261
262
|
|
|
262
263
|
```bash
|
|
263
264
|
uv tool install 'insightfactory-cli[mcp]'
|
|
264
265
|
uvx --from 'insightfactory-cli[mcp]' if-cli mcp foundry
|
|
265
266
|
```
|
|
266
267
|
|
|
268
|
+
The first `uvx` launch resolves and downloads the package from PyPI before it runs, which
|
|
269
|
+
added roughly four to six seconds on top of the usual startup in local testing, against
|
|
270
|
+
under two seconds once `uv`'s cache was warm. A client with a fixed handshake budget
|
|
271
|
+
(Claude Code allows 30 seconds) usually has room for that, but a slow network can close
|
|
272
|
+
the gap. Two ways to avoid paying it on every launch: `uv tool install 'insightfactory-cli[mcp]'`
|
|
273
|
+
puts a real script on `PATH` with no resolve step, or run the same `uvx --from ...` command
|
|
274
|
+
by hand once before pointing a client at it, which warms `uv`'s cache for the launches after.
|
|
275
|
+
|
|
267
276
|
Add it to `.mcp.json`:
|
|
268
277
|
|
|
269
278
|
```json
|
|
@@ -290,6 +299,7 @@ passed at all, replace the section's value outright, and `--allow-tool` is alway
|
|
|
290
299
|
| `--read-only` | Forces every environment read-only, overriding both the section's `writable` and any `--writable`. Cannot be combined with `--writable`. |
|
|
291
300
|
| `--catalog CODE` (default: the first environment) | Whose tool list is published; its schemas are the reference every environment is checked against. |
|
|
292
301
|
| `--allow-tool NAME` (repeatable) | Extra tool names treated as read-only, beyond the shipped list. |
|
|
302
|
+
| `--skills` / `--no-skills` | Whether to advertise the experimental skills extension. Off unless the section says `skills = true` or `--skills` is passed. The two cannot be combined. |
|
|
293
303
|
| `--timeout SECONDS` (default `120`) | Per upstream request; higher than the `api` command's default because some tools are slow. |
|
|
294
304
|
|
|
295
305
|
`mcp`'s `--timeout` does not read `INSIGHTFACTORY_REQUEST_TIMEOUT`: it parses the flag with
|
|
@@ -304,6 +314,62 @@ Passing dev-only writes to production means changing an argument value, not
|
|
|
304
314
|
connecting to a different server, so keep `--writable` scoped to the
|
|
305
315
|
environments meant to be written by hand.
|
|
306
316
|
|
|
317
|
+
### Resources, prompts and completion
|
|
318
|
+
|
|
319
|
+
Resources and prompts are not merged across environments the way tools are, because
|
|
320
|
+
a factory's non-tool content splits in two.
|
|
321
|
+
|
|
322
|
+
`skill://` bodies are documentation that belongs to a release, identical on every
|
|
323
|
+
environment running it, and they cross-reference each other by bare URI. So they are
|
|
324
|
+
served from the `--catalog` environment exactly as published. Rewriting those URIs to
|
|
325
|
+
carry an environment would leave every link inside them pointing at nothing.
|
|
326
|
+
|
|
327
|
+
A resource template is the other half: `if://task-config-schemas/{taskType}` resolves
|
|
328
|
+
to one factory's own activity catalogue, not to a shared document. The router
|
|
329
|
+
republishes each template with its own variable appended, so the client chooses the
|
|
330
|
+
environment when it expands the URI:
|
|
331
|
+
|
|
332
|
+
```text
|
|
333
|
+
if://task-config-schemas/{taskType}{?environment}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
A template that already opens a query gets `{&environment}` instead, and one that
|
|
337
|
+
ends in a fragment gets the variable in front of it, because a `?` after a `#` is an
|
|
338
|
+
ordinary character rather than a query. A template that already declares an
|
|
339
|
+
`environment` variable is skipped with a line on stderr, so one nonconforming item
|
|
340
|
+
does not hide the rest of the list.
|
|
341
|
+
|
|
342
|
+
Reading a URI with no `?environment=` is answered by the catalogue environment,
|
|
343
|
+
unless the URI could be an expansion of a published template, which is refused
|
|
344
|
+
naming the codes to choose from. Membership of `resources/list` is deliberately not
|
|
345
|
+
the test: a skill body may link to a sibling document the factory never advertised,
|
|
346
|
+
and such a link carries no `environment` and could not be given one.
|
|
347
|
+
`completion/complete` reads the environment from the request's context arguments,
|
|
348
|
+
then from the reference URI, and falls back to the catalogue environment for anything
|
|
349
|
+
it does not recognise, because a half-typed context value is the normal state of a
|
|
350
|
+
completion request and completion should degrade rather than fail. The router answers
|
|
351
|
+
for the `environment` variable itself rather than asking a factory about an argument
|
|
352
|
+
it has never heard of.
|
|
353
|
+
|
|
354
|
+
Prompts gain a required `environment` argument like tools. The router also prepends a
|
|
355
|
+
line to every rendered prompt naming the environment chosen, because a factory writes
|
|
356
|
+
its prompts for a client talking to one factory and they tell the model to call tools
|
|
357
|
+
without one. Prepended rather than appended, and marked `[if-cli router]`: the
|
|
358
|
+
factory's own last turn may be an assistant prefill, which a trailing user turn would
|
|
359
|
+
silently end.
|
|
360
|
+
|
|
361
|
+
`--skills` only controls the advertisement. The handshake is answered before any
|
|
362
|
+
factory has been reached, so the router cannot ask the catalogue environment whether
|
|
363
|
+
it serves skills in time to repeat the claim, and an experimental capability is not
|
|
364
|
+
worth guessing at. The `skill://` resources are served either way.
|
|
365
|
+
|
|
366
|
+
The advertisement reaches only a client on MCP protocol 2026-07-28 or later.
|
|
367
|
+
`capabilities.extensions` was added in that revision, and the `initialize` handshake
|
|
368
|
+
every current client opens with negotiates a 2025 revision whose capability schema
|
|
369
|
+
has no such field, so the SDK strips it before it reaches the wire. Against
|
|
370
|
+
`foundryaz-dev` with `--skills` on, Claude Code sees no extension and still finds
|
|
371
|
+
every skill through `resources/list`, which is how clients discover them today.
|
|
372
|
+
|
|
307
373
|
A tool the factory will not run until someone finishes a step in a browser, such
|
|
308
374
|
as linking a personal Databricks identity, comes back as a failed call naming the
|
|
309
375
|
page to visit rather than as a prompt. The router never relays the factory's
|
|
@@ -354,20 +420,32 @@ artifact, so its notes belong on that Release. A checkout with no tags falls bac
|
|
|
354
420
|
to the tagged version's section alone.
|
|
355
421
|
|
|
356
422
|
`develop` therefore carries the version the next release will use, and the
|
|
357
|
-
`release-guard` job holds it there.
|
|
358
|
-
|
|
359
|
-
`CHANGELOG.md` section. `tools/check_release.py` runs both checks and is what
|
|
360
|
-
fails the PR:
|
|
423
|
+
`release-guard` job holds it there. `tools/check_release.py` runs the checks and
|
|
424
|
+
is what fails the PR:
|
|
361
425
|
|
|
362
426
|
```bash
|
|
363
|
-
BASE=origin/develop python3 tools/check_release.py
|
|
427
|
+
BASE=origin/develop RELEASED=origin/main python3 tools/check_release.py
|
|
364
428
|
```
|
|
365
429
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
430
|
+
**Every PR into `develop` adds a `CHANGELOG.md` entry.** That is the whole point
|
|
431
|
+
of the file, and a change with no entry reaches users with no line anywhere
|
|
432
|
+
saying it happened.
|
|
433
|
+
|
|
434
|
+
**The version moves once per release cycle, not once per PR.** The first PR after
|
|
435
|
+
a release raises `version` in `pyproject.toml` and opens that version's section;
|
|
436
|
+
the rest of the cycle adds to the section it opened. The guard asks for the bump
|
|
437
|
+
only while `develop` is still on the version `main` is on, because that version
|
|
438
|
+
is released and its notes describe something your change is not in.
|
|
439
|
+
|
|
440
|
+
The comparison is against `main` rather than against the tag, which matters in
|
|
441
|
+
the window where the release PR has merged but the tag is not pushed yet. What a
|
|
442
|
+
version contains is fixed when it reaches `main`, so a change landing after that
|
|
443
|
+
belongs to the next version, whatever the tags say.
|
|
444
|
+
|
|
445
|
+
A PR into `main` always needs the bump. It carries no new entries, only the
|
|
446
|
+
version they were written under, and `main` exists to be tagged: leaving it on
|
|
447
|
+
`main`'s own version makes it un-taggable, since that version is on PyPI and any
|
|
448
|
+
other tag matches no file.
|
|
371
449
|
|
|
372
450
|
Verify and build live in the reusable workflow
|
|
373
451
|
[`if_s_insightfactory_cli.yml`](https://github.com/insightfactory-ai/if_sre_github_actions/blob/main/.github/workflows/if_s_insightfactory_cli.yml)
|
|
@@ -231,15 +231,24 @@ tst = example-tst
|
|
|
231
231
|
writable = dev
|
|
232
232
|
```
|
|
233
233
|
|
|
234
|
-
Every key except `writable` and `
|
|
235
|
-
order is the environment order. `writable` is a comma-separated list of codes (default
|
|
236
|
-
none); `catalog` is one code (default the first environment)
|
|
234
|
+
Every key except `writable`, `catalog` and `skills` is `<environment code> = <profile name>`;
|
|
235
|
+
file order is the environment order. `writable` is a comma-separated list of codes (default
|
|
236
|
+
none); `catalog` is one code (default the first environment); `skills` is `true` or `false`
|
|
237
|
+
(default false). Then:
|
|
237
238
|
|
|
238
239
|
```bash
|
|
239
240
|
uv tool install 'insightfactory-cli[mcp]'
|
|
240
241
|
uvx --from 'insightfactory-cli[mcp]' if-cli mcp foundry
|
|
241
242
|
```
|
|
242
243
|
|
|
244
|
+
The first `uvx` launch resolves and downloads the package from PyPI before it runs, which
|
|
245
|
+
added roughly four to six seconds on top of the usual startup in local testing, against
|
|
246
|
+
under two seconds once `uv`'s cache was warm. A client with a fixed handshake budget
|
|
247
|
+
(Claude Code allows 30 seconds) usually has room for that, but a slow network can close
|
|
248
|
+
the gap. Two ways to avoid paying it on every launch: `uv tool install 'insightfactory-cli[mcp]'`
|
|
249
|
+
puts a real script on `PATH` with no resolve step, or run the same `uvx --from ...` command
|
|
250
|
+
by hand once before pointing a client at it, which warms `uv`'s cache for the launches after.
|
|
251
|
+
|
|
243
252
|
Add it to `.mcp.json`:
|
|
244
253
|
|
|
245
254
|
```json
|
|
@@ -266,6 +275,7 @@ passed at all, replace the section's value outright, and `--allow-tool` is alway
|
|
|
266
275
|
| `--read-only` | Forces every environment read-only, overriding both the section's `writable` and any `--writable`. Cannot be combined with `--writable`. |
|
|
267
276
|
| `--catalog CODE` (default: the first environment) | Whose tool list is published; its schemas are the reference every environment is checked against. |
|
|
268
277
|
| `--allow-tool NAME` (repeatable) | Extra tool names treated as read-only, beyond the shipped list. |
|
|
278
|
+
| `--skills` / `--no-skills` | Whether to advertise the experimental skills extension. Off unless the section says `skills = true` or `--skills` is passed. The two cannot be combined. |
|
|
269
279
|
| `--timeout SECONDS` (default `120`) | Per upstream request; higher than the `api` command's default because some tools are slow. |
|
|
270
280
|
|
|
271
281
|
`mcp`'s `--timeout` does not read `INSIGHTFACTORY_REQUEST_TIMEOUT`: it parses the flag with
|
|
@@ -280,6 +290,62 @@ Passing dev-only writes to production means changing an argument value, not
|
|
|
280
290
|
connecting to a different server, so keep `--writable` scoped to the
|
|
281
291
|
environments meant to be written by hand.
|
|
282
292
|
|
|
293
|
+
### Resources, prompts and completion
|
|
294
|
+
|
|
295
|
+
Resources and prompts are not merged across environments the way tools are, because
|
|
296
|
+
a factory's non-tool content splits in two.
|
|
297
|
+
|
|
298
|
+
`skill://` bodies are documentation that belongs to a release, identical on every
|
|
299
|
+
environment running it, and they cross-reference each other by bare URI. So they are
|
|
300
|
+
served from the `--catalog` environment exactly as published. Rewriting those URIs to
|
|
301
|
+
carry an environment would leave every link inside them pointing at nothing.
|
|
302
|
+
|
|
303
|
+
A resource template is the other half: `if://task-config-schemas/{taskType}` resolves
|
|
304
|
+
to one factory's own activity catalogue, not to a shared document. The router
|
|
305
|
+
republishes each template with its own variable appended, so the client chooses the
|
|
306
|
+
environment when it expands the URI:
|
|
307
|
+
|
|
308
|
+
```text
|
|
309
|
+
if://task-config-schemas/{taskType}{?environment}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
A template that already opens a query gets `{&environment}` instead, and one that
|
|
313
|
+
ends in a fragment gets the variable in front of it, because a `?` after a `#` is an
|
|
314
|
+
ordinary character rather than a query. A template that already declares an
|
|
315
|
+
`environment` variable is skipped with a line on stderr, so one nonconforming item
|
|
316
|
+
does not hide the rest of the list.
|
|
317
|
+
|
|
318
|
+
Reading a URI with no `?environment=` is answered by the catalogue environment,
|
|
319
|
+
unless the URI could be an expansion of a published template, which is refused
|
|
320
|
+
naming the codes to choose from. Membership of `resources/list` is deliberately not
|
|
321
|
+
the test: a skill body may link to a sibling document the factory never advertised,
|
|
322
|
+
and such a link carries no `environment` and could not be given one.
|
|
323
|
+
`completion/complete` reads the environment from the request's context arguments,
|
|
324
|
+
then from the reference URI, and falls back to the catalogue environment for anything
|
|
325
|
+
it does not recognise, because a half-typed context value is the normal state of a
|
|
326
|
+
completion request and completion should degrade rather than fail. The router answers
|
|
327
|
+
for the `environment` variable itself rather than asking a factory about an argument
|
|
328
|
+
it has never heard of.
|
|
329
|
+
|
|
330
|
+
Prompts gain a required `environment` argument like tools. The router also prepends a
|
|
331
|
+
line to every rendered prompt naming the environment chosen, because a factory writes
|
|
332
|
+
its prompts for a client talking to one factory and they tell the model to call tools
|
|
333
|
+
without one. Prepended rather than appended, and marked `[if-cli router]`: the
|
|
334
|
+
factory's own last turn may be an assistant prefill, which a trailing user turn would
|
|
335
|
+
silently end.
|
|
336
|
+
|
|
337
|
+
`--skills` only controls the advertisement. The handshake is answered before any
|
|
338
|
+
factory has been reached, so the router cannot ask the catalogue environment whether
|
|
339
|
+
it serves skills in time to repeat the claim, and an experimental capability is not
|
|
340
|
+
worth guessing at. The `skill://` resources are served either way.
|
|
341
|
+
|
|
342
|
+
The advertisement reaches only a client on MCP protocol 2026-07-28 or later.
|
|
343
|
+
`capabilities.extensions` was added in that revision, and the `initialize` handshake
|
|
344
|
+
every current client opens with negotiates a 2025 revision whose capability schema
|
|
345
|
+
has no such field, so the SDK strips it before it reaches the wire. Against
|
|
346
|
+
`foundryaz-dev` with `--skills` on, Claude Code sees no extension and still finds
|
|
347
|
+
every skill through `resources/list`, which is how clients discover them today.
|
|
348
|
+
|
|
283
349
|
A tool the factory will not run until someone finishes a step in a browser, such
|
|
284
350
|
as linking a personal Databricks identity, comes back as a failed call naming the
|
|
285
351
|
page to visit rather than as a prompt. The router never relays the factory's
|
|
@@ -330,20 +396,32 @@ artifact, so its notes belong on that Release. A checkout with no tags falls bac
|
|
|
330
396
|
to the tagged version's section alone.
|
|
331
397
|
|
|
332
398
|
`develop` therefore carries the version the next release will use, and the
|
|
333
|
-
`release-guard` job holds it there.
|
|
334
|
-
|
|
335
|
-
`CHANGELOG.md` section. `tools/check_release.py` runs both checks and is what
|
|
336
|
-
fails the PR:
|
|
399
|
+
`release-guard` job holds it there. `tools/check_release.py` runs the checks and
|
|
400
|
+
is what fails the PR:
|
|
337
401
|
|
|
338
402
|
```bash
|
|
339
|
-
BASE=origin/develop python3 tools/check_release.py
|
|
403
|
+
BASE=origin/develop RELEASED=origin/main python3 tools/check_release.py
|
|
340
404
|
```
|
|
341
405
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
406
|
+
**Every PR into `develop` adds a `CHANGELOG.md` entry.** That is the whole point
|
|
407
|
+
of the file, and a change with no entry reaches users with no line anywhere
|
|
408
|
+
saying it happened.
|
|
409
|
+
|
|
410
|
+
**The version moves once per release cycle, not once per PR.** The first PR after
|
|
411
|
+
a release raises `version` in `pyproject.toml` and opens that version's section;
|
|
412
|
+
the rest of the cycle adds to the section it opened. The guard asks for the bump
|
|
413
|
+
only while `develop` is still on the version `main` is on, because that version
|
|
414
|
+
is released and its notes describe something your change is not in.
|
|
415
|
+
|
|
416
|
+
The comparison is against `main` rather than against the tag, which matters in
|
|
417
|
+
the window where the release PR has merged but the tag is not pushed yet. What a
|
|
418
|
+
version contains is fixed when it reaches `main`, so a change landing after that
|
|
419
|
+
belongs to the next version, whatever the tags say.
|
|
420
|
+
|
|
421
|
+
A PR into `main` always needs the bump. It carries no new entries, only the
|
|
422
|
+
version they were written under, and `main` exists to be tagged: leaving it on
|
|
423
|
+
`main`'s own version makes it un-taggable, since that version is on PyPI and any
|
|
424
|
+
other tag matches no file.
|
|
347
425
|
|
|
348
426
|
Verify and build live in the reusable workflow
|
|
349
427
|
[`if_s_insightfactory_cli.yml`](https://github.com/insightfactory-ai/if_sre_github_actions/blob/main/.github/workflows/if_s_insightfactory_cli.yml)
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import importlib.util
|
|
4
|
+
import platform
|
|
4
5
|
import sys
|
|
5
6
|
|
|
6
7
|
from if_cli.cli import Options, parse_args
|
|
7
8
|
from if_cli.config import config_file, get_profile, list_factories, load_config, parse_factory_section
|
|
8
9
|
from if_cli.http import parse_timeout_seconds
|
|
10
|
+
from if_cli.router.log import log
|
|
9
11
|
from if_cli.runtime import die
|
|
10
12
|
|
|
11
13
|
MCP_OPTIONS: Options = {
|
|
@@ -15,12 +17,15 @@ MCP_OPTIONS: Options = {
|
|
|
15
17
|
"catalog": {"type": "string"},
|
|
16
18
|
"allow-tool": {"type": "string", "repeat": True},
|
|
17
19
|
"timeout": {"type": "string", "default": "120"},
|
|
20
|
+
"skills": {"type": "boolean"},
|
|
21
|
+
"no-skills": {"type": "boolean"},
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
MCP_USAGE = (
|
|
21
25
|
"usage: if-cli mcp [FACTORY] [--env CODE=PROFILE ...]\n"
|
|
22
26
|
" [--writable CODE ... | --read-only] [--catalog CODE]\n"
|
|
23
27
|
" [--allow-tool NAME ...] [--timeout SECONDS]\n"
|
|
28
|
+
" [--skills | --no-skills]\n"
|
|
24
29
|
"\n"
|
|
25
30
|
"Runs one MCP server over stdio that fronts several factory environments: every\n"
|
|
26
31
|
"tool gets a required 'environment' argument, routed to that environment's own\n"
|
|
@@ -29,7 +34,9 @@ MCP_USAGE = (
|
|
|
29
34
|
"section. Without FACTORY, --env is required at least once. --catalog defaults\n"
|
|
30
35
|
"to the first environment; environments not passed to --writable are read-only;\n"
|
|
31
36
|
"--read-only forces every environment read-only and cannot be combined with\n"
|
|
32
|
-
"--writable
|
|
37
|
+
"--writable. --skills advertises the experimental skills extension; the\n"
|
|
38
|
+
"catalog environment's skill:// resources are served either way. It is off\n"
|
|
39
|
+
"unless the factory section says 'skills = true' or --skills is passed.\n"
|
|
33
40
|
)
|
|
34
41
|
|
|
35
42
|
MISSING_MCP_EXTRA = (
|
|
@@ -70,6 +77,8 @@ def _stray_positional(rest: list[str]) -> str | None:
|
|
|
70
77
|
|
|
71
78
|
|
|
72
79
|
def mcp_command(argv: list[str]) -> None:
|
|
80
|
+
log(f"starting on Python {platform.python_version()}")
|
|
81
|
+
|
|
73
82
|
if "-h" in argv or "--help" in argv:
|
|
74
83
|
sys.stdout.write(MCP_USAGE)
|
|
75
84
|
return
|
|
@@ -91,6 +100,9 @@ def mcp_command(argv: list[str]) -> None:
|
|
|
91
100
|
if read_only and raw_writable is not None:
|
|
92
101
|
die(f"--read-only cannot be combined with --writable\n\n{MCP_USAGE}")
|
|
93
102
|
|
|
103
|
+
if values["skills"] and values["no-skills"]:
|
|
104
|
+
die(f"--skills cannot be combined with --no-skills\n\n{MCP_USAGE}")
|
|
105
|
+
|
|
94
106
|
config = load_config()
|
|
95
107
|
|
|
96
108
|
# A dict, not a list of pairs: --env "adds or replaces that code's profile"
|
|
@@ -102,12 +114,14 @@ def mcp_command(argv: list[str]) -> None:
|
|
|
102
114
|
mappings: dict[str, str] = {}
|
|
103
115
|
writable_codes: frozenset[str] = frozenset()
|
|
104
116
|
catalog_source: str | None = None
|
|
117
|
+
skills = False
|
|
105
118
|
|
|
106
119
|
if factory_name is not None:
|
|
107
120
|
factory = parse_factory_section(config, factory_name)
|
|
108
121
|
mappings.update(factory["environments"])
|
|
109
122
|
writable_codes = factory["writable"]
|
|
110
123
|
catalog_source = factory["catalog"]
|
|
124
|
+
skills = factory["skills"]
|
|
111
125
|
|
|
112
126
|
for raw in raw_envs:
|
|
113
127
|
code, profile_name = _parse_env_mapping(raw)
|
|
@@ -125,6 +139,10 @@ def mcp_command(argv: list[str]) -> None:
|
|
|
125
139
|
writable_codes = frozenset(raw_writable)
|
|
126
140
|
if values["catalog"] is not None:
|
|
127
141
|
catalog_source = values["catalog"]
|
|
142
|
+
if values["skills"]:
|
|
143
|
+
skills = True
|
|
144
|
+
elif values["no-skills"]:
|
|
145
|
+
skills = False
|
|
128
146
|
if catalog_source is None:
|
|
129
147
|
catalog_source = codes[0]
|
|
130
148
|
|
|
@@ -132,7 +150,6 @@ def mcp_command(argv: list[str]) -> None:
|
|
|
132
150
|
# import bug inside if_cli.router propagates instead of reading as a missing extra.
|
|
133
151
|
if importlib.util.find_spec("mcp") is None:
|
|
134
152
|
die(MISSING_MCP_EXTRA)
|
|
135
|
-
from if_cli.router.log import log
|
|
136
153
|
from if_cli.router.server import build_router, run_server
|
|
137
154
|
|
|
138
155
|
timeout = parse_timeout_seconds(values["timeout"], "--timeout")
|
|
@@ -153,7 +170,7 @@ def mcp_command(argv: list[str]) -> None:
|
|
|
153
170
|
f"{code}={mappings[code]} {profiles[code]['host']}{' (writable)' if code in writable_codes else ''}"
|
|
154
171
|
for code in codes
|
|
155
172
|
]
|
|
156
|
-
log(f"{label}: {'; '.join(parts)}; catalog={catalog_source}")
|
|
173
|
+
log(f"{label}: {'; '.join(parts)}; catalog={catalog_source}; skills={'on' if skills else 'off'}")
|
|
157
174
|
|
|
158
175
|
router = build_router(
|
|
159
176
|
profiles=profiles,
|
|
@@ -161,5 +178,6 @@ def mcp_command(argv: list[str]) -> None:
|
|
|
161
178
|
catalog_source=catalog_source,
|
|
162
179
|
extra_read_only=extra_read_only,
|
|
163
180
|
timeout=timeout,
|
|
181
|
+
skills=skills,
|
|
164
182
|
)
|
|
165
183
|
run_server(router)
|
|
@@ -28,10 +28,13 @@ class Factory(TypedDict):
|
|
|
28
28
|
environments: list[tuple[str, str]]
|
|
29
29
|
writable: frozenset[str]
|
|
30
30
|
catalog: str
|
|
31
|
+
skills: bool
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
FACTORY_SECTION_PREFIX = "factory "
|
|
34
|
-
FACTORY_RESERVED_KEYS = {"writable", "catalog"}
|
|
35
|
+
FACTORY_RESERVED_KEYS = {"writable", "catalog", "skills"}
|
|
36
|
+
FACTORY_TRUE = {"true", "yes", "on", "1"}
|
|
37
|
+
FACTORY_FALSE = {"false", "no", "off", "0", ""}
|
|
35
38
|
|
|
36
39
|
|
|
37
40
|
def config_dir() -> str:
|
|
@@ -217,7 +220,18 @@ def parse_factory_section(config: dict[str, dict[str, str]], name: str) -> Facto
|
|
|
217
220
|
catalog_value = section.get("catalog")
|
|
218
221
|
catalog = catalog_value.strip() if catalog_value is not None and catalog_value.strip() else codes[0]
|
|
219
222
|
|
|
220
|
-
|
|
223
|
+
skills_value = section.get("skills", "").strip().lower()
|
|
224
|
+
if skills_value not in FACTORY_TRUE and skills_value not in FACTORY_FALSE:
|
|
225
|
+
die(f"factory '{name}' in {config_file()}: skills must be true or false (found '{skills_value}')")
|
|
226
|
+
skills = skills_value in FACTORY_TRUE
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
"name": name,
|
|
230
|
+
"environments": environments,
|
|
231
|
+
"writable": writable,
|
|
232
|
+
"catalog": catalog,
|
|
233
|
+
"skills": skills,
|
|
234
|
+
}
|
|
221
235
|
|
|
222
236
|
|
|
223
237
|
def get_factory(config: dict[str, dict[str, str]], name: str) -> Factory:
|