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.
Files changed (77) hide show
  1. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/.gitignore +1 -0
  2. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/CHANGELOG.md +44 -2
  3. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/CLAUDE.md +10 -6
  4. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/PKG-INFO +92 -14
  5. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/README.md +91 -13
  6. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/pyproject.toml +1 -1
  7. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/commands/mcp.py +21 -3
  8. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/config.py +16 -2
  9. insightfactory_cli-1.1.2/src/if_cli/router/content.py +298 -0
  10. insightfactory_cli-1.1.2/src/if_cli/router/log.py +20 -0
  11. insightfactory_cli-1.1.2/src/if_cli/router/server.py +594 -0
  12. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/router/upstream.py +113 -8
  13. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_config.py +30 -0
  14. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_mcp_command.py +216 -7
  15. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_release_tools.py +136 -8
  16. insightfactory_cli-1.1.2/tests/test_router_content.py +310 -0
  17. insightfactory_cli-1.1.2/tests/test_router_log.py +23 -0
  18. insightfactory_cli-1.1.2/tests/test_router_resources.py +436 -0
  19. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_router_upstream.py +36 -5
  20. insightfactory_cli-1.1.2/tools/check_release.py +163 -0
  21. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/uv.lock +1 -1
  22. insightfactory_cli-1.1.1.dev25/src/if_cli/router/log.py +0 -13
  23. insightfactory_cli-1.1.1.dev25/src/if_cli/router/server.py +0 -281
  24. insightfactory_cli-1.1.1.dev25/tools/check_release.py +0 -79
  25. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/.github/workflows/ci.yml +0 -0
  26. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/.github/workflows/claude.yml +0 -0
  27. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/.github/workflows/release.yml +0 -0
  28. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/.python-version +0 -0
  29. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/AGENTS.md +0 -0
  30. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/LICENSE +0 -0
  31. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/__init__.py +0 -0
  32. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/__main__.py +0 -0
  33. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/assets/__init__.py +0 -0
  34. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/assets/insightfactoryai-logo.svg +0 -0
  35. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/cache.py +0 -0
  36. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/callback_page.py +0 -0
  37. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/cli.py +0 -0
  38. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/colour.py +0 -0
  39. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/commands/__init__.py +0 -0
  40. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/commands/api.py +0 -0
  41. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/commands/config.py +0 -0
  42. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/commands/login.py +0 -0
  43. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/commands/logout.py +0 -0
  44. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/commands/profiles.py +0 -0
  45. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/commands/set_token.py +0 -0
  46. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/commands/token.py +0 -0
  47. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/constants.py +0 -0
  48. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/http.py +0 -0
  49. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/main.py +0 -0
  50. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/oauth.py +0 -0
  51. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/router/__init__.py +0 -0
  52. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/router/catalog.py +0 -0
  53. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/router/policy.py +0 -0
  54. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/src/if_cli/runtime.py +0 -0
  55. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/__init__.py +0 -0
  56. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/cache_writer.py +0 -0
  57. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/conftest.py +0 -0
  58. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/helpers.py +0 -0
  59. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/servers.py +0 -0
  60. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_api.py +0 -0
  61. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_api_command.py +0 -0
  62. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_cache.py +0 -0
  63. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_cli.py +0 -0
  64. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_config_command.py +0 -0
  65. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_login.py +0 -0
  66. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_oauth.py +0 -0
  67. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_oauth_flow.py +0 -0
  68. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_oauth_force_refresh.py +0 -0
  69. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_profiles.py +0 -0
  70. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_programmatic_api.py +0 -0
  71. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_router_catalog.py +0 -0
  72. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_router_policy.py +0 -0
  73. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_router_server.py +0 -0
  74. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_runtime.py +0 -0
  75. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_set_token.py +0 -0
  76. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tests/test_token.py +0 -0
  77. {insightfactory_cli-1.1.1.dev25 → insightfactory_cli-1.1.2}/tools/release_notes.py +0 -0
@@ -9,3 +9,4 @@ __pycache__/
9
9
  dist/
10
10
  build/
11
11
  .DS_Store
12
+ .tmp/
@@ -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: a PR into `develop` or `main` must raise the version and add its section here,
11
- and a tag whose version has no section fails rather than releasing an empty page.
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` or `main` raises `version` in `pyproject.toml` and
38
- documents that version in `CHANGELOG.md`.** The `release-guard` job runs
39
- `tools/check_release.py` and fails the PR otherwise. Put user-visible changes in
40
- the Added/Changed/Fixed group that fits; for internal-only work (tests, CI, a
41
- refactor nothing observes) a one-line entry saying so is enough. The section
42
- becomes the body of that version's GitHub Release.
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.1.dev25
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 `catalog` is `<environment code> = <profile name>`; file
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). Then:
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. A PR into `develop` must raise `version` in
358
- `pyproject.toml` above what `develop` already has, and must add that version's
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
- One version per PR, then. The instinct is to let a second change join the
367
- section the first one opened, and the guard refuses that: the version would be
368
- tagged with notes that do not describe everything in it, and nothing stops the
369
- tag going out between the two merges. Bump the patch number again, or fold the
370
- second change into the first PR.
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 `catalog` is `<environment code> = <profile name>`; file
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). Then:
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. A PR into `develop` must raise `version` in
334
- `pyproject.toml` above what `develop` already has, and must add that version's
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
- One version per PR, then. The instinct is to let a second change join the
343
- section the first one opened, and the guard refuses that: the version would be
344
- tagged with notes that do not describe everything in it, and nothing stops the
345
- tag going out between the two merges. Bump the patch number again, or fold the
346
- second change into the first PR.
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "insightfactory-cli"
3
- version = "1.1.1.dev25"
3
+ version = "1.1.2"
4
4
  description = "Profile-based authentication CLI for the InsightFactory Interfaces API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -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.\n"
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
- return {"name": name, "environments": environments, "writable": writable, "catalog": catalog}
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: