fix(onboarding): consume future catalog entries without custom metadata
This commit is contained in:
@@ -24,6 +24,11 @@ function entry(name: string, patch: Partial<McpCatalogEntry> = {}): McpCatalogEn
|
||||
}
|
||||
|
||||
it('recommends catalog entries from evidence and intent, never from installation as proof of access', () => {
|
||||
const future = entry('future-paint', { suggest: null, detected_apps: ['Future Paint'] })
|
||||
expect(onboardingRecommendations([future])).toMatchObject([{
|
||||
name: future.name, description: future.description, examples: [], setupAction: 'install'
|
||||
}])
|
||||
expect(onboardingRecommendations([])).toEqual([])
|
||||
const local = entry('modeler', { detected_apps: ['Modeler'] })
|
||||
const newEntry = entry('future-studio', { detected_apps: ['Future Studio'] })
|
||||
const longerExample = 'Describe the lighting and materials in my scene before suggesting a different render treatment'
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface OnboardingInterests {
|
||||
|
||||
export interface OnboardingRecommendation {
|
||||
name: string
|
||||
description: string
|
||||
examples: string[]
|
||||
detectedApps: string[]
|
||||
readiness: 'configured_unverified' | 'setup_required'
|
||||
@@ -17,7 +18,7 @@ export interface OnboardingRecommendation {
|
||||
|
||||
const words = (text: string): string => text.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean).join(' ')
|
||||
|
||||
/** Rank evidence, not a fixed list of products. The model personalizes these reviewed outcomes later. */
|
||||
/** Rank evidence, not a fixed list of products. The model derives outcomes from catalog descriptions; curated examples are optional. */
|
||||
export function onboardingRecommendations(
|
||||
entries: readonly McpCatalogEntry[],
|
||||
{ apps = [], context = '' }: OnboardingInterests = {}
|
||||
@@ -28,10 +29,6 @@ export function onboardingRecommendations(
|
||||
const candidates = entries.flatMap(entry => {
|
||||
const examples = [...new Set(entry.suggest?.examples ?? [])].filter(text => text.trim())
|
||||
|
||||
if (!examples.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const terms = [entry.name, ...(entry.suggest?.keywords ?? []), ...(entry.suggest?.applications ?? [])].map(words).filter(Boolean)
|
||||
const preferred = terms.some(term => selected.has(term))
|
||||
const topical = terms.some(term => subject.includes(` ${term} `))
|
||||
@@ -53,6 +50,7 @@ export function onboardingRecommendations(
|
||||
|
||||
const recommendation: OnboardingRecommendation = {
|
||||
name: entry.name,
|
||||
description: entry.description,
|
||||
examples: examples.slice(0, preferred || topical ? 3 : 1),
|
||||
detectedApps,
|
||||
readiness: configured ? 'configured_unverified' : 'setup_required',
|
||||
|
||||
@@ -20,7 +20,7 @@ export async function readOnboardingCapabilities(scope?: ProfileScope, interests
|
||||
}))),
|
||||
'Detection describes the backend host where its MCP runs, not necessarily the desktop computer. Configured does not mean connected or working; setup_required means permission and prerequisites are still needed. Read the full catalog setup instructions and verify the connection before using it. Missing detection is unknown, not proof an app is absent.',
|
||||
'Only offer setup-dependent suggestions when manage_connections is actually available in this session. If it is absent or a connection is refused, do not route around that through the CLI or a second integration. Configured entries still need their actual tools to be available. Catalog presence is not an entitlement or a successful connection.',
|
||||
'Use the reviewed examples as starting points, then favor the user’s stated work and app choices. Detection earns at most one option per app; do not turn its example into several variants. The rest of the menu comes from the user’s goals and other capabilities. More options within one app are appropriate only when explicitly requested. Do not replace the fresh-machine or Spark setup fork. Keep a connection-free choice. Carry the exact MCP name in the handoff brief when one of these tasks is chosen; the task session uses manage_connections with name and mcp:true.'
|
||||
'Derive useful tasks from the actual catalog descriptions and the user’s stated work and app choices. Curated examples are optional; their absence must not hide an otherwise relevant catalog entry. Never invent an integration absent from the catalog. Detection earns at most one option per app; do not turn its example into several variants. The rest of the menu comes from the user’s goals and other capabilities. More options within one app are appropriate only when explicitly requested. Do not replace the fresh-machine or Spark setup fork. Keep a connection-free choice. Carry the exact MCP name in the handoff brief when one of these tasks is chosen; the task session uses manage_connections with name and mcp:true.'
|
||||
].join(' ')
|
||||
} catch {
|
||||
// An older/unreachable catalog must not block the existing welcome or handoff path.
|
||||
|
||||
@@ -418,7 +418,9 @@ async def list_mcp_catalog(profile: Optional[str] = None, detect_apps: bool = Fa
|
||||
# Discovery is read-only and backend-local. Keep filesystem work off the
|
||||
# event loop and OUTSIDE the profile/skills lock used for config reads.
|
||||
detected = await asyncio.to_thread(discover_catalog_apps, {
|
||||
entry["name"]: (entry["suggest"] or {}).get("applications", []) for entry in entries
|
||||
entry["name"]: (entry["suggest"] or {}).get("applications") or [
|
||||
entry["name"].replace("-", " ").replace("_", " ")
|
||||
] for entry in entries
|
||||
})
|
||||
except Exception:
|
||||
_log.warning("Backend application discovery unavailable")
|
||||
|
||||
@@ -103,6 +103,19 @@ def test_catalog_detection_is_opt_in_and_preserves_profile_state(catalog_client,
|
||||
assert data == old.json()
|
||||
assert len(calls) == before + 1
|
||||
assert {directory: (directory / "config.yaml").read_bytes() for directory in snapshots} == snapshots
|
||||
future = tmp_path / "catalog" / "fixture-paint"
|
||||
future.mkdir()
|
||||
manifest = future / "manifest.yaml"
|
||||
manifest.write_text(yaml.safe_dump({
|
||||
"manifest_version": 1, "name": "fixture-paint", "description": "Create illustrations in Fixture Paint",
|
||||
"transport": {"type": "stdio", "command": "must-not-run"},
|
||||
}), encoding="utf-8")
|
||||
fresh = client.get("/api/mcp/catalog?detect_apps=true").json()
|
||||
added = next(entry for entry in fresh["entries"] if entry["name"] == "fixture-paint")
|
||||
assert added["suggest"] is None
|
||||
assert added["detected_apps"] == ["fixture paint"]
|
||||
manifest.unlink()
|
||||
assert all(entry["name"] != "fixture-paint" for entry in client.get("/api/mcp/catalog?detect_apps=true").json()["entries"])
|
||||
assert client.get("/api/mcp/catalog", params={"profile": "missing", "detect_apps": True}).status_code == 404
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@ Otherwise the guide favors relevant app-backed tasks, with a connection-free alt
|
||||
|
||||
## Catalog metadata
|
||||
|
||||
Extend the reviewed `optional-mcps/<name>/manifest.yaml`, not a product-name switch in the renderer:
|
||||
Read the actual catalog at runtime. An entry does not need new recommendation metadata: detection falls back to its catalog name (with slug separators treated as spaces), and the model derives tasks from its description. Removing an entry removes it from future snapshots. No product-specific entry, package or install recipe belongs in the onboarding change.
|
||||
|
||||
Catalog owners may optionally provide richer hints:
|
||||
|
||||
```yaml
|
||||
suggest:
|
||||
@@ -39,7 +41,7 @@ The managed-app picker keeps its curated leaders while making other enabled rows
|
||||
|
||||
`GET /api/mcp/catalog?detect_apps=true` adds:
|
||||
|
||||
- `entries[].detected_apps`: matching reviewed application labels only.
|
||||
- `entries[].detected_apps`: matching catalog-derived application names or optional explicit aliases only.
|
||||
- `discovery`: `{scope: "backend", status: "ok" | "unavailable", platform: string}`.
|
||||
|
||||
The default catalog request performs no app discovery. The optional scan checks standard application locations and exact safe PATH candidates on the **backend machine**, where its MCP processes run. It never launches apps, starts MCPs, installs packages, reads application documents or contacts the network. It returns neither a full inventory nor filesystem paths.
|
||||
@@ -78,4 +80,4 @@ There is one intentional product-policy overlap in the onboarding runbooks and t
|
||||
|
||||
Contract tests cover catalogue parsing, opt-in read-only discovery, A/B/A profile isolation, missing metadata, ranking, backend-pinned seed creation, legal MCP setup actions and preservation of fresh-machine/Spark priority.
|
||||
|
||||
Live inference over synthetic onboarding turns can verify the recommended outcome and generated setup action. Native discovery can verify a real installed app signal. Neither is proof of OAuth completion or execution inside Blender: that still requires the application's add-on/server, the user's approval and a harmless live tool check. See the Blender catalog entry's setup notes for its prerequisites.
|
||||
Live inference over synthetic onboarding turns can verify the recommended outcome and generated setup action. Native discovery can verify a real installed app signal. Neither is proof of OAuth completion or execution inside Blender: that still requires the application's add-on/server, the user's approval and a harmless live tool check. Read the actual returned entry's setup notes; onboarding does not supply its own integration recipe.
|
||||
|
||||
Reference in New Issue
Block a user