insightfactory-cli 1.1.2.dev29__tar.gz → 1.1.2.dev31__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.2.dev29 → insightfactory_cli-1.1.2.dev31}/.gitignore +1 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/CHANGELOG.md +13 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/PKG-INFO +9 -1
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/README.md +8 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/pyproject.toml +1 -1
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/mcp.py +4 -1
- insightfactory_cli-1.1.2.dev31/src/if_cli/router/log.py +20 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/router/server.py +24 -1
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/router/upstream.py +44 -1
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_mcp_command.py +6 -6
- insightfactory_cli-1.1.2.dev31/tests/test_router_log.py +23 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_router_upstream.py +36 -5
- insightfactory_cli-1.1.2.dev29/src/if_cli/router/log.py +0 -13
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/.github/workflows/ci.yml +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/.github/workflows/claude.yml +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/.github/workflows/release.yml +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/.python-version +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/AGENTS.md +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/CLAUDE.md +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/LICENSE +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/__init__.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/__main__.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/assets/__init__.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/assets/insightfactoryai-logo.svg +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/cache.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/callback_page.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/cli.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/colour.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/__init__.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/api.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/config.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/login.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/logout.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/profiles.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/set_token.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/token.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/config.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/constants.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/http.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/main.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/oauth.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/router/__init__.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/router/catalog.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/router/content.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/router/policy.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/runtime.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/__init__.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/cache_writer.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/conftest.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/helpers.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/servers.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_api.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_api_command.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_cache.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_cli.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_config.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_config_command.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_login.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_oauth.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_oauth_flow.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_oauth_force_refresh.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_profiles.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_programmatic_api.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_release_tools.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_router_catalog.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_router_content.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_router_policy.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_router_resources.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_router_server.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_runtime.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_set_token.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_token.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tools/check_release.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tools/release_notes.py +0 -0
- {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/uv.lock +0 -0
|
@@ -31,6 +31,12 @@ last one has been released, so a cycle's PRs share a section.
|
|
|
31
31
|
|
|
32
32
|
### Changed
|
|
33
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.
|
|
34
40
|
- `release-guard` asks every PR into `develop` for a `CHANGELOG.md` entry, and asks
|
|
35
41
|
for a version bump only when `develop` is still on the version `main` is on. One
|
|
36
42
|
bump opens a release cycle and the PRs after it add to the section it opened,
|
|
@@ -39,6 +45,13 @@ last one has been released, so a cycle's PRs share a section.
|
|
|
39
45
|
has to land above the released version, not merely above the base, and a PR into an
|
|
40
46
|
open cycle has to add a line rather than reword one.
|
|
41
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.
|
|
54
|
+
|
|
42
55
|
## 1.1.1
|
|
43
56
|
|
|
44
57
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: insightfactory-cli
|
|
3
|
-
Version: 1.1.2.
|
|
3
|
+
Version: 1.1.2.dev31
|
|
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>
|
|
@@ -265,6 +265,14 @@ uv tool install 'insightfactory-cli[mcp]'
|
|
|
265
265
|
uvx --from 'insightfactory-cli[mcp]' if-cli mcp foundry
|
|
266
266
|
```
|
|
267
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
|
+
|
|
268
276
|
Add it to `.mcp.json`:
|
|
269
277
|
|
|
270
278
|
```json
|
|
@@ -241,6 +241,14 @@ uv tool install 'insightfactory-cli[mcp]'
|
|
|
241
241
|
uvx --from 'insightfactory-cli[mcp]' if-cli mcp foundry
|
|
242
242
|
```
|
|
243
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
|
+
|
|
244
252
|
Add it to `.mcp.json`:
|
|
245
253
|
|
|
246
254
|
```json
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/mcp.py
RENAMED
|
@@ -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 = {
|
|
@@ -75,6 +77,8 @@ def _stray_positional(rest: list[str]) -> str | None:
|
|
|
75
77
|
|
|
76
78
|
|
|
77
79
|
def mcp_command(argv: list[str]) -> None:
|
|
80
|
+
log(f"starting on Python {platform.python_version()}")
|
|
81
|
+
|
|
78
82
|
if "-h" in argv or "--help" in argv:
|
|
79
83
|
sys.stdout.write(MCP_USAGE)
|
|
80
84
|
return
|
|
@@ -146,7 +150,6 @@ def mcp_command(argv: list[str]) -> None:
|
|
|
146
150
|
# import bug inside if_cli.router propagates instead of reading as a missing extra.
|
|
147
151
|
if importlib.util.find_spec("mcp") is None:
|
|
148
152
|
die(MISSING_MCP_EXTRA)
|
|
149
|
-
from if_cli.router.log import log
|
|
150
153
|
from if_cli.router.server import build_router, run_server
|
|
151
154
|
|
|
152
155
|
timeout = parse_timeout_seconds(values["timeout"], "--timeout")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
# Captured at import: this module loads before the router does anything else
|
|
7
|
+
# diagnosable, so it stands in for process start. Two `log()` lines can then
|
|
8
|
+
# be subtracted to time the phase between them.
|
|
9
|
+
_START = time.monotonic()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def log(message: str) -> None:
|
|
13
|
+
"""Write one timestamped diagnostic line to stderr.
|
|
14
|
+
|
|
15
|
+
The router speaks MCP over stdout, so stderr is the only channel that can
|
|
16
|
+
carry anything to the operator. This lives in its own module because both
|
|
17
|
+
`server` and `upstream` write to it and `server` imports `upstream`.
|
|
18
|
+
"""
|
|
19
|
+
elapsed_ms = round((time.monotonic() - _START) * 1000)
|
|
20
|
+
sys.stderr.write(f"[if-cli mcp] +{elapsed_ms}ms {message}\n")
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/router/server.py
RENAMED
|
@@ -9,7 +9,7 @@ from typing import Any, TypeVar
|
|
|
9
9
|
import anyio
|
|
10
10
|
import mcp_types as types
|
|
11
11
|
from mcp.server import Server
|
|
12
|
-
from mcp.server.context import ServerRequestContext
|
|
12
|
+
from mcp.server.context import CallNext, HandlerResult, ServerMiddleware, ServerRequestContext
|
|
13
13
|
from mcp.server.stdio import stdio_server
|
|
14
14
|
from mcp.shared.exceptions import MCPError
|
|
15
15
|
|
|
@@ -137,6 +137,7 @@ class Router:
|
|
|
137
137
|
|
|
138
138
|
async def build_catalog(self) -> list[types.Tool]:
|
|
139
139
|
"""Build the merged catalogue. Safe to call again to pick up a new login."""
|
|
140
|
+
log(f"catalogue: building from {self.config.catalog_source}")
|
|
140
141
|
catalogues = await self._catalogues()
|
|
141
142
|
source = catalogues.get(self.config.catalog_source)
|
|
142
143
|
if source is None:
|
|
@@ -536,6 +537,27 @@ def advertised_extensions(router: Router) -> dict[str, dict[str, Any]]:
|
|
|
536
537
|
return {SKILLS_EXTENSION: {}} if router.config.skills else {}
|
|
537
538
|
|
|
538
539
|
|
|
540
|
+
def _log_first_initialize() -> ServerMiddleware[Any]:
|
|
541
|
+
"""Log once the router has answered a client's `initialize`.
|
|
542
|
+
|
|
543
|
+
Read against `prime_catalog`'s startup log lines, this tells an operator
|
|
544
|
+
apart a slow process (nothing logged yet), a slow factory (catalogue
|
|
545
|
+
build still running past this point), and a client that called before
|
|
546
|
+
the handshake (a tool call logged before this line ever appears).
|
|
547
|
+
"""
|
|
548
|
+
logged = False
|
|
549
|
+
|
|
550
|
+
async def middleware(ctx: ServerRequestContext[Any, Any], call_next: CallNext) -> HandlerResult:
|
|
551
|
+
nonlocal logged
|
|
552
|
+
result = await call_next(ctx)
|
|
553
|
+
if not logged and ctx.method == "initialize":
|
|
554
|
+
logged = True
|
|
555
|
+
log("answered the first initialize")
|
|
556
|
+
return result
|
|
557
|
+
|
|
558
|
+
return middleware
|
|
559
|
+
|
|
560
|
+
|
|
539
561
|
async def _serve(router: Router) -> None:
|
|
540
562
|
server: Server[Any] = Server(
|
|
541
563
|
SERVER_NAME,
|
|
@@ -553,6 +575,7 @@ async def _serve(router: Router) -> None:
|
|
|
553
575
|
# the handshake rebuilds its capabilities from `server.extensions` once the
|
|
554
576
|
# protocol version is known, and ignores the ones computed here.
|
|
555
577
|
server.extensions = advertised_extensions(router)
|
|
578
|
+
server.middleware.append(_log_first_initialize())
|
|
556
579
|
# Answer `initialize` immediately: the catalogue build runs as a background
|
|
557
580
|
# task so one hung factory reads as a slow tools/list, not a failed
|
|
558
581
|
# handshake (clients cap startup time).
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/router/upstream.py
RENAMED
|
@@ -138,6 +138,22 @@ class ConnectUrlRecorder:
|
|
|
138
138
|
return types.ElicitResult(action="decline")
|
|
139
139
|
|
|
140
140
|
|
|
141
|
+
def _has_url_request(result: types.InputRequiredResult) -> bool:
|
|
142
|
+
"""Whether any of the result's requests is the Databricks connect flow.
|
|
143
|
+
|
|
144
|
+
A request in any other mode (today, only `form`) gates a preview behind a
|
|
145
|
+
confirmation rather than asking for a trip to a browser, so its presence
|
|
146
|
+
alone never puts the result on the connect path. A URL-mode request does,
|
|
147
|
+
even one whose URL fails the origin check in `_connect_url` and so
|
|
148
|
+
contributes no link: the call still did not run, and the caller still
|
|
149
|
+
needs the existing refusal, not a result that reads as success.
|
|
150
|
+
"""
|
|
151
|
+
return any(
|
|
152
|
+
isinstance(getattr(request, "params", None), types.ElicitRequestURLParams)
|
|
153
|
+
for request in (result.input_requests or {}).values()
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
141
157
|
def _relayable_requests(result: types.InputRequiredResult, origin: str) -> tuple[list[str], str | None]:
|
|
142
158
|
"""The messages this router will repeat, and the first connect URL it will name.
|
|
143
159
|
|
|
@@ -279,7 +295,16 @@ class Upstream:
|
|
|
279
295
|
result = await self._run(call, elicitation_callback=recorder)
|
|
280
296
|
if isinstance(result, types.InputRequiredResult):
|
|
281
297
|
lines, url = _relayable_requests(result, self.profile["host"])
|
|
282
|
-
|
|
298
|
+
if _has_url_request(result):
|
|
299
|
+
return self._connect_refusal(lines, url)
|
|
300
|
+
# Every mutating tool gates a `confirmed:false` call behind a form-mode
|
|
301
|
+
# elicitation once the router advertises elicitation at all (see
|
|
302
|
+
# `ConnectUrlRecorder`). That gate is a preview, not a connect flow: the
|
|
303
|
+
# factory's own answer to a client with no elicitation capability is a
|
|
304
|
+
# plain `pending_confirmation` result, and the router owes the caller
|
|
305
|
+
# the closest thing it can relay to that, not an error pointing at a
|
|
306
|
+
# web UI the caller was never asked to visit.
|
|
307
|
+
return self._preview_result(lines)
|
|
283
308
|
if recorder.declined_url_request:
|
|
284
309
|
# The older-revision path. The server elicited in band and carried on
|
|
285
310
|
# once the router declined, so the connect page is in the recorder
|
|
@@ -290,6 +315,24 @@ class Upstream:
|
|
|
290
315
|
return self._connect_refusal(_result_texts(result), recorder.url)
|
|
291
316
|
return result
|
|
292
317
|
|
|
318
|
+
def _preview_result(self, lines: list[str]) -> types.CallToolResult:
|
|
319
|
+
"""A form-mode approval gate, relayed as the ordinary result it is.
|
|
320
|
+
|
|
321
|
+
This loses the structured `pending_confirmation` JSON a client with no
|
|
322
|
+
elicitation capability would see (the `approvalId`, `preview`,
|
|
323
|
+
`impactedResources`): the router has only the elicitation's message
|
|
324
|
+
text to work with, not the tool's own result body, which the factory
|
|
325
|
+
never sends once it has decided to gate the call. The message still
|
|
326
|
+
carries what a caller needs, and the caller still proceeds the same
|
|
327
|
+
way, by calling again with `confirmed:true`.
|
|
328
|
+
|
|
329
|
+
Not an error: the call is still pending, exactly as it would be for a
|
|
330
|
+
client that never advertised elicitation, and marking this `is_error`
|
|
331
|
+
would make a normal preview indistinguishable from the connect refusal
|
|
332
|
+
it is not.
|
|
333
|
+
"""
|
|
334
|
+
return types.CallToolResult(content=[types.TextContent(text="\n".join(lines))], is_error=False)
|
|
335
|
+
|
|
293
336
|
def _connect_refusal(self, lines: list[str], url: str | None) -> types.CallToolResult:
|
|
294
337
|
"""Turn the factory's request for input into a refusal naming the page to visit.
|
|
295
338
|
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import re
|
|
5
6
|
import subprocess
|
|
6
7
|
import sys
|
|
7
8
|
import time
|
|
@@ -802,11 +803,10 @@ def test_mcp_command_logs_the_resolved_factory_mapping_at_startup(tmp_path: Path
|
|
|
802
803
|
|
|
803
804
|
anyio.run(scenario)
|
|
804
805
|
stderr_text = stderr_path.read_text(encoding="utf-8")
|
|
805
|
-
expected = (
|
|
806
|
-
f"
|
|
807
|
-
f"tst=profile-b {server_b.origin}; catalog=dev"
|
|
806
|
+
expected = re.escape(
|
|
807
|
+
f"foundry: dev=profile-a {server_a.origin} (writable); tst=profile-b {server_b.origin}; catalog=dev"
|
|
808
808
|
)
|
|
809
|
-
assert expected
|
|
809
|
+
assert re.search(rf"\[if-cli mcp\] \+\d+ms {expected}", stderr_text)
|
|
810
810
|
finally:
|
|
811
811
|
server_a.close()
|
|
812
812
|
server_b.close()
|
|
@@ -827,8 +827,8 @@ def test_mcp_command_logs_environments_label_without_a_factory(tmp_path: Path):
|
|
|
827
827
|
|
|
828
828
|
anyio.run(scenario)
|
|
829
829
|
stderr_text = stderr_path.read_text(encoding="utf-8")
|
|
830
|
-
expected = f"
|
|
831
|
-
assert expected
|
|
830
|
+
expected = re.escape(f"environments: a=profile-a {server.origin} (writable); catalog=a")
|
|
831
|
+
assert re.search(rf"\[if-cli mcp\] \+\d+ms {expected}", stderr_text)
|
|
832
832
|
finally:
|
|
833
833
|
server.close()
|
|
834
834
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from if_cli.router import log as log_module
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_log_prefixes_the_elapsed_time_since_process_start(monkeypatch, capsys):
|
|
7
|
+
monkeypatch.setattr(log_module, "_START", 10.0)
|
|
8
|
+
monkeypatch.setattr(log_module.time, "monotonic", lambda: 10.25)
|
|
9
|
+
|
|
10
|
+
log_module.log("ready over stdio")
|
|
11
|
+
|
|
12
|
+
assert capsys.readouterr().err == "[if-cli mcp] +250ms ready over stdio\n"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_log_keeps_the_grep_prefix_first(monkeypatch, capsys):
|
|
16
|
+
"""`[if-cli mcp]` must stay the first thing on the line, timestamp or not:
|
|
17
|
+
it is what an operator (or a script) greps a stdio transcript for."""
|
|
18
|
+
monkeypatch.setattr(log_module, "_START", 0.0)
|
|
19
|
+
monkeypatch.setattr(log_module.time, "monotonic", lambda: 0.0)
|
|
20
|
+
|
|
21
|
+
log_module.log("WARN dropped a connect URL")
|
|
22
|
+
|
|
23
|
+
assert capsys.readouterr().err.startswith("[if-cli mcp] +0ms ")
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_router_upstream.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
+
import re
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
7
|
import pytest
|
|
@@ -253,21 +254,49 @@ def test_call_tool_takes_the_first_same_origin_url():
|
|
|
253
254
|
]
|
|
254
255
|
|
|
255
256
|
|
|
256
|
-
def
|
|
257
|
-
"""
|
|
257
|
+
def test_call_tool_relays_a_form_mode_request_as_a_preview_not_an_error():
|
|
258
|
+
"""A `confirmed:false` mutating call gates behind a form-mode elicitation once
|
|
259
|
+
the router advertises elicitation at all; it is a preview, not a connect
|
|
260
|
+
flow, so it comes back as an ordinary result the caller can act on by
|
|
261
|
+
calling again with `confirmed:true`."""
|
|
258
262
|
form = types.ElicitRequest(
|
|
259
263
|
params=types.ElicitRequestFormParams(
|
|
260
|
-
message="
|
|
264
|
+
message="Ready to add **1** task(s) to production line **NEIL_ROUTER_DEMO**. Ask the user to confirm.",
|
|
265
|
+
requested_schema=types.ElicitRequestedSchema(properties={}),
|
|
261
266
|
)
|
|
262
267
|
)
|
|
263
268
|
upstream, _ = _upstream_returning(_input_required(form))
|
|
264
269
|
|
|
270
|
+
result = anyio.run(lambda: upstream.call_tool("add_tasks_to_production_line", {}))
|
|
271
|
+
|
|
272
|
+
assert not result.is_error
|
|
273
|
+
assert _texts(result) == [
|
|
274
|
+
"Ready to add **1** task(s) to production line **NEIL_ROUTER_DEMO**. Ask the user to confirm."
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_call_tool_treats_a_form_and_url_mix_as_the_connect_flow():
|
|
279
|
+
"""A URL-mode request anywhere in the result keeps today's connect refusal,
|
|
280
|
+
even alongside a form-mode one: the connect flow must behave exactly as
|
|
281
|
+
before, so its presence, not the absence of other modes, decides the
|
|
282
|
+
outcome. The form's own message still rides along as an extra relayed
|
|
283
|
+
line, same as any other request that is not the chosen connect URL."""
|
|
284
|
+
form = types.ElicitRequest(
|
|
285
|
+
params=types.ElicitRequestFormParams(
|
|
286
|
+
message="Pick one.", requested_schema=types.ElicitRequestedSchema(properties={})
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
upstream, _ = _upstream_returning(
|
|
290
|
+
_input_required(form, _url_request("https://factory.example/my-profile/developer"))
|
|
291
|
+
)
|
|
292
|
+
|
|
265
293
|
result = anyio.run(lambda: upstream.call_tool("run_sql_query", {}))
|
|
266
294
|
|
|
267
295
|
assert result.is_error
|
|
268
296
|
assert _texts(result) == [
|
|
269
297
|
"Pick one.",
|
|
270
|
-
"
|
|
298
|
+
"Connect your Databricks account.",
|
|
299
|
+
"[prd] Connect at https://factory.example/my-profile/developer, then retry.",
|
|
271
300
|
]
|
|
272
301
|
|
|
273
302
|
|
|
@@ -345,7 +374,9 @@ def test_a_dropped_connect_url_is_reported_on_stderr(capsys):
|
|
|
345
374
|
anyio.run(lambda: upstream.call_tool("run_sql_query", {}))
|
|
346
375
|
|
|
347
376
|
stderr = capsys.readouterr().err
|
|
348
|
-
assert
|
|
377
|
+
assert re.search(
|
|
378
|
+
r"\[if-cli mcp\] \+\d+ms WARN dropped a connect URL that is not on https://factory\.example", stderr
|
|
379
|
+
)
|
|
349
380
|
assert "https://phish.example/connect" in stderr
|
|
350
381
|
|
|
351
382
|
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import sys
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def log(message: str) -> None:
|
|
7
|
-
"""Write one diagnostic line to stderr.
|
|
8
|
-
|
|
9
|
-
The router speaks MCP over stdout, so stderr is the only channel that can
|
|
10
|
-
carry anything to the operator. This lives in its own module because both
|
|
11
|
-
`server` and `upstream` write to it and `server` imports `upstream`.
|
|
12
|
-
"""
|
|
13
|
-
sys.stderr.write(f"[if-cli mcp] {message}\n")
|
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/.github/workflows/claude.yml
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/.github/workflows/release.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/assets/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/callback_page.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/__init__.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/api.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/config.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/login.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/logout.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/profiles.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/set_token.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/token.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/router/__init__.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/router/catalog.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/router/content.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/src/if_cli/router/policy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_config_command.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_oauth_force_refresh.py
RENAMED
|
File without changes
|
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_programmatic_api.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_release_tools.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_router_catalog.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_router_content.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_router_policy.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_router_resources.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev31}/tests/test_router_server.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|