Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion gecko/catalog_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
from .caller import CallError
from .mcp_server import _SEARCH_TOOL, to_lightweight_ref
from .modes import CallMode
from .paysh_catalog import CatalogRegistry
from .paysh_catalog import CatalogRegistry, ProviderSurface
from .surfaces import SurfaceError, safe_surface_id

# How many providers a cross-catalog search returns. Small: the agent wants the few most
# relevant providers, not a marketplace dump.
Expand Down Expand Up @@ -101,6 +102,54 @@ def get_capability(self, name: str) -> dict[str, Any]:
raise CallError(f"unknown tool: {name!r}")
return ps.client.get_tool(name)

# -- resolve (§13 Phase 3.2 — the context7 resolve-library-id analogue) -------
def enumerate_providers(self) -> list[str]:
"""The fqns of the providers registered in THIS workspace's registry (sorted).

Scoped to what the operator provisioned — NEVER a hosted/global public catalog.
The `surfaces.py` non-goal holds: exposing a global ``ids()`` would make us a
marketplace; this only enumerates the workspace's own connected providers, the
enumerate half of the resolve step."""
return sorted(ps.entry.fqn for ps in self.registry.providers())

def resolve_provider(self, name: str) -> ProviderSurface | None:
"""Resolve a provider by human name to its comprehended surface, or None.

Deterministic, no fuzzy guessing: an exact fqn, then an exact tool slug, then a
unique slug match against the tool name / fqn / title. An unknown or AMBIGUOUS
name returns None (never a marketplace dump). Resolution is scoped to the
workspace registry — the context7 ``resolve-library-id`` analogue, but over the
providers this workspace connected, not a global index."""
if not name or not name.strip():
return None
exact = self.registry.get(name) or self.registry.by_tool(name)
if exact is not None:
return exact
try:
slug = safe_surface_id(name)
except SurfaceError:
return None
by_slug = self.registry.by_tool(slug)
if by_slug is not None:
return by_slug
matches = [
ps
for ps in self.registry.providers()
if slug in (safe_surface_id(ps.entry.fqn), safe_surface_id(ps.entry.title))
]
return matches[0] if len(matches) == 1 else None

# -- the value-domain index over every connected provider (§13 Phase 3.3) -----
def value_domain_index(self) -> Any:
"""The ``entity -> {producers, consumers}`` index across every connected
provider — the cross-provider "what correlates with X" lookup. Deterministic,
control-plane only. Returns a :class:`~gecko.vindex.ValueDomainIndex`."""
from .surface import Surface
from .vindex import value_domain_index

surfaces = [Surface.of(ps.client) for ps in self.registry.providers()]
return value_domain_index(surfaces)

def call_tool(
self, name: str, arguments: dict[str, Any], session_id: str | None = None
) -> Any:
Expand Down
35 changes: 35 additions & 0 deletions gecko/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"auth",
"graph",
"correlate",
"index",
"rm",
"list",
"doctor",
Expand Down Expand Up @@ -630,6 +631,38 @@ def _cmd_correlate(argv: list[str]) -> int:
return 0


def _cmd_index(argv: list[str]) -> int:
"""`gecko index <specA> <specB> [...]` — the multi-provider value-domain index.

Thin transport, like `gecko correlate`: build a Surface per spec, then report the
deterministic ``entity -> {producers, consumers}`` map + the cross-provider joins as
JSON. A cross-provider join is plan-eligible ONLY when both sides are customer-
confirmed (§13.6); a provider-only declaration is a quarantined candidate. No vectors:
the index is a strict lookup by DECLARED entity. This is a report — non-zero exit is
not used.
"""
from .access import public_session
from .surface import Surface
from .vindex import value_domain_index

p = argparse.ArgumentParser(
prog="gecko index",
description="Report the value-domain index across N surfaces, with provenance.",
)
p.add_argument("specs", nargs="+", help="Two+ OpenAPI URLs, paths, or docs URLs.")
args = p.parse_args(argv)
if len(args.specs) < 2:
print("index: need at least two specs to correlate.", file=sys.stderr)
return 2

surfaces = [
Surface.from_spec(spec, session=public_session(), surface_id=f"s{i}")
for i, spec in enumerate(args.specs)
]
print(json.dumps(value_domain_index(surfaces).to_dict(), indent=2))
return 0


def _cmd_auth(argv: list[str]) -> int:
"""`gecko auth set|rm|list` — thin transport over ``credentials`` (keychain).

Expand Down Expand Up @@ -1343,6 +1376,8 @@ def main(argv: list[str] | None = None) -> int:
return _cmd_graph(rest)
if cmd == "correlate":
return _cmd_correlate(rest)
if cmd == "index":
return _cmd_index(rest)
if cmd == "rm":
return _cmd_rm(rest)
if cmd == "list":
Expand Down
Loading
Loading