The lean profile has grown to 10 tools across two parallel paradigms (NL-first auto vs explicit pipeline), creating decision fatigue for agents. auto without server_hint is unreliable (Issue #34). The goal is a 5-tool lean surface where every tool is deterministic, the mental model is a single linear flow, and agents never have to decide "which of these 3 similar tools do I pick?"
Flow after this change: status → search → auth → shapeshift → call → shapeshift()
| Tool | Replaces / absorbs |
|---|---|
status() |
unchanged + inline first-run guide (replaces onboard() pointer) |
search(query, compare=False) |
absorbs compare() via flag |
auth(server_id_or_var, value="") |
NEW — replaces key + login |
shapeshift(server_id="", keep=False, ...) |
combined mount + unmount (replaces shiftback in lean) |
call(tool, arguments) |
unchanged |
Forge-only (still fully functional, just not default): auto, inspect, compare, key, onboard, shiftback, login, all existing forge tools.
server_idnon-empty → mount (existing logic unchanged)server_idempty → unmount:- Default (
keep=False): kill process + uninstall package (clean slate, next mount always fresh) keep=True: unmount tools from surface, keep pool warm + package cached (re-attach soon)
- Default (
shiftback()stays in forge with its existingkill=False, uninstall=Falsedefaults for backward compat
valueprovided → store as env var (same askey()):auth("EXA_API_KEY", "sk-...")- Name is ALL_CAPS → env var status check + prompt:
auth("EXA_API_KEY")→ "not set, use auth('EXA_API_KEY', 'val')" - Name is lowercase/hyphenated → server ID:
- stdio with credentials → list missing vars + guide
- stdio without credentials → "no auth needed, ready to shapeshift"
- http transport → OAuth: call
ensure_token(base_url)(handles refresh, full flow if needed) - Not found →
search("name")suggestion
compare=False(default): ranked list (existing search behavior)compare=True: side-by-side table (existing compare behavior, calls extracted_run_compare())- New footer:
auth('<id>') to check credentials | shapeshift('<id>') to mount
- Replace
"✨ New here? Run onboard() for the 3-step quickstart."with inline quickstart:✨ Quick start: 1. shapeshift("mcp-server-time") 2. call("get_current_time", {"timezone": "UTC"}) 3. shapeshift() More: search("what you need") | auth("SMITHERY_API_KEY", "sm-...") for 3000+ servers
- Rename file:
git mv kitsune_mcp/tools/morph.py kitsune_mcp/tools/shapeshift.py - Update
kitsune_mcp/tools/__init__.py: changefrom kitsune_mcp.tools.morph import ...→from kitsune_mcp.tools.shapeshift import ...- Note: no conflict with
kitsune_mcp/shapeshift.py(top-level proxy module) — this lives intools/
- Note: no conflict with
- Change
shapeshift(server_id: str, ctx, ...)→shapeshift(server_id: str = "", ctx, keep: bool = False, ...) - Add branch at top:
if not server_id: return await _do_unmount(ctx, keep=keep) - Add
_do_unmount(ctx, keep)helper:keep=True: run existingshiftback()body withkill=False, uninstall=Falsekeep=False: run existingshiftback()body withkill=True, uninstall=True- Update "still cached" hint:
"To clean up: shapeshift()"instead of"shiftback(uninstall=True)"
- Keep
shiftback()unchanged (forge backward compat)
- Remove
login()(never wired to server.py) - Add
auth()(new tool, see behavior above) - Update
_KITSUNE_LEANfrozenset insideauto():frozenset({"auth", "call", "search", "shapeshift", "status"}) - Update
_KITSUNE_FORGEto include:"auto", "compare", "inspect", "key", "login", "onboard", "shiftback" - Keep
key()unchanged (forge)
search(): addcompare: bool = Falseparam; when True delegate to_run_compare(query, limit)- Extract
compare()body → private_run_compare(query, limit, probe=False) compare()becomes:return await _run_compare(query, limit, probe)- Update
search()footer:inspect('<id>') for details→auth('<id>') to check credentials | shapeshift('<id>') to mount - Update
status()first-run block: inline quickstart (noonboard()pointer) - Update all
key("FOO", "...")hint strings →auth("FOO", "...")(in_compare_probeaction strings +inspect()next-step hints)
- Add
"auth"to_BASE_TOOL_NAMESset - Keep
"login"in_BASE_TOOL_NAMESfor backward compat (won't be registered if not in profile)
- Change morph import → shapeshift import (step 1 above)
- Add
authto import line:from kitsune_mcp.tools.onboarding import auth, auto, key, onboard, setup, skill - Remove
login(not exported)
_LEAN_TOOLS = {"status", "search", "auth", "shapeshift", "call"}- Add
authto the import block - Update header docstring: lean profile now 5 tools (~400 tokens)
LEAN_REQUIRED = {"shapeshift", "search", "auth", "call", "status"}FORGE_ONLYadd:"shiftback", "auto", "inspect", "compare", "key", "onboard"- Remove
test_compare_is_in_lean()test - Update count assertion:
len(_LEAN_TOOLS) == 5
| File | Old | New |
|---|---|---|
tests/test_ux_changes.py:188 |
"onboard()" in result |
"shapeshift(" in result |
tests/test_ux_changes.py:655 |
"shiftback(uninstall=True)" in result |
"shapeshift()" in result |
tests/test_ux_changes.py:874 |
'key("API_KEY"' in result |
'auth("API_KEY"' in result |
tests/test_ux_changes.py:1293 |
'key("NOTION_LOCAL_OPS_WORKSPACE_ROOT"' in result |
'auth("NOTION_LOCAL_OPS_WORKSPACE_ROOT"' in result |
tests/test_probe.py:193 |
'key("EXA_API_KEY"' in result |
'auth("EXA_API_KEY"' in result |
tests/test_issues_12_13_14_15.py:156 |
"key(" in result |
"auth(" in result |
tests/test_tools.py:115 |
"key(" in result |
"auth(" in result |
tests/test_tools.py:614 |
"key(" in result |
"auth(" in result |
tests/test_v014_fixes.py:158 |
"onboard()" in result |
"shapeshift(" in result |
tests/test_v014_fixes.py:220 |
"onboard" in result.lower() |
"search(" in result or "auth(" in result |
- Bump
pyproject.tomlto0.18.0 - Add
loginto_BASE_TOOL_NAMESfor completeness but don't register it authin_KITSUNE_BUILTINSguard insideauto()preventsauto("auth")routing to an external server
# 1. All tests pass
python3 -m pytest tests/ -x -q
# 2. Lean surface is exactly 5 tools
python3 -c "from server import _LEAN_TOOLS; print(sorted(_LEAN_TOOLS))"
# Expected: ['auth', 'call', 'search', 'shapeshift', 'status']
# 3. Restart MCP server, then via MCP tools:
status() # first-run quickstart shows inline
search("time zones") # ranked list
search("time zones", compare=True) # table view
auth("mcp-server-time") # → "no auth needed"
auth("SMITHERY_API_KEY", "sm-...") # → "Saved: ..."
shapeshift("mcp-server-time") # mount
call("get_current_time", {"timezone": "UTC"}) # use
shapeshift() # unmount + kill + uninstall (default)
shapeshift("mcp-server-time") # re-mount pulls fresh version
shapeshift(keep=True) # unmount but keep warm