Discovery & Bridge
SLOP consumers need to find and connect to providers. A consumer SDK handles the wire protocol (subscribe, query, invoke). The discovery layer sits above it and handles everything else: finding providers, managing connections, bridging browser tabs, and formatting state for AI consumption.
This document specifies the discovery layer’s behavior in a language-agnostic way. Each SDK implements the same semantics in its own idioms.
Architecture
Section titled “Architecture”┌──────────────────────────────────────────────────────┐│ Integration Layer (Claude plugin, OpenClaw, etc.) ││ Thin wrapper: exposes tools, injects context │└──────────────┬───────────────────────────────────────┘ │ uses┌──────────────▼───────────────────────────────────────┐│ Discovery Layer ││ Provider scanning, bridge client/server, ││ relay transport, auto-connect, state formatting │└──────────────┬───────────────────────────────────────┘ │ uses┌──────────────▼───────────────────────────────────────┐│ Consumer SDK ││ SlopConsumer: connect, subscribe, query, invoke ││ Transports: WebSocket, Unix socket, stdio │└──────────────────────────────────────────────────────┘The consumer SDK is intentionally minimal — a pure SLOP protocol client with pluggable transports. The discovery layer adds the intelligence: where are providers, how do I connect to them, what if I need to bridge through an extension?
Integrations (Claude Code plugin, OpenClaw plugin, VS Code extension, custom agents) are thin wrappers that expose discovery capabilities to their specific host environment.
TypeScript package exports (@slop-ai/discovery)
Section titled “TypeScript package exports (@slop-ai/discovery)”| Entry | Purpose |
|---|---|
@slop-ai/discovery | Default. Discovery service, bridge, relay, CLI, and agent-agnostic helpers: createToolHandlers, createDynamicTools, createStateCache, etc. No Anthropic-specific dependency at import time for these APIs. |
@slop-ai/discovery/anthropic-agent-sdk | Optional. createSlopAgentTools and createSlopMcpServer for @anthropic-ai/claude-agent-sdk (query(), programmatic MCP). Use only when wiring the Anthropic Agent SDK. |
Integrations that only need discovery + MCP (e.g. slop-bridge with the MCP SDK) import from the default export and do not need anthropic-agent-sdk.
Phase 1: Core Discovery Layer
Section titled “Phase 1: Core Discovery Layer”The current cross-SDK parity target is the core discovery layer. The TypeScript implementation is the behavioral reference, but other SDKs do not need to copy its package topology.
- TypeScript reference files:
packages/typescript/integrations/discovery/src/discovery.ts,bridge-client.ts,bridge-server.ts,relay-transport.ts, andtools.ts - Python target module:
slop_ai.discovery - Go target package:
github.com/devteapot/slop/packages/go/slop-ai/discovery - Rust target module:
slop_ai::discovery
Python, Go, and Rust keep discovery inside their existing SDK artifact. Only TypeScript publishes discovery as a separate package.
Phase 1 scope
Section titled “Phase 1 scope”Phase 1 includes:
- Local provider scanning (
~/.slop/providers/+/tmp/slop/providers/) - Descriptor validation and pruning when descriptors disappear
- Directory watching with periodic fallback scan
- Bridge client
- Bridge server
- “Try client first, fall back to server” bridge startup
- Relay transport for
postmessageproviders - Discovery service / connection orchestration
- Lazy connect with
ensureConnected() - Auto-connect mode
- Idle timeout (5 minutes default)
- Exponential backoff reconnection
- Connection timeout (10 seconds)
- State change callback
formatTree()affordancesToTools()createToolHandlers()createDynamicTools()
Phase 1 explicitly excludes:
- Host-specific wrappers such as
cli.tsandanthropic-agent-sdk.ts - Prompt/hook integration glue for Claude Code, Codex, OpenClaw, or other hosts
- File-cache helpers such as
createStateCache()
Status labels
Section titled “Status labels”| Label | Meaning |
|---|---|
Shipped | Implemented in the SDK artifact with the current contract |
Partial | Code exists, but only in app code or with behavior drift from the contract |
Planned | Not implemented yet |
Out of scope | Intentionally excluded from phase 1 |
Provider Discovery
Section titled “Provider Discovery”Local providers
Section titled “Local providers”Applications register themselves by writing a JSON descriptor file to one of two directories:
{ "id": "my-app", "name": "My Application", "slop_version": "0.1", "transport": { "type": "unix", "path": "/tmp/slop/my-app.sock" }, "pid": 12345, "capabilities": ["state", "patches", "affordances"]}| Directory | Purpose |
|---|---|
~/.slop/providers/ | Persistent user-level providers (desktop apps, daemons) |
/tmp/slop/providers/ | Session-level ephemeral providers (dev servers, CLI tools) |
The discovery layer:
- Scans both directories for
*.jsonfiles on startup - Watches both directories for changes (file add/remove)
- Re-scans periodically (every 15 seconds) as a fallback
- Removes providers whose descriptor files disappear
Supported transport types in descriptors:
| Type | Field | Description |
|---|---|---|
unix | path | Unix domain socket path |
ws | url | WebSocket endpoint URL |
stdio | — | Standard input/output (reserved for CLI tools) |
Browser providers (via extension bridge)
Section titled “Browser providers (via extension bridge)”Browser tabs running SLOP-enabled SPAs expose providers through the Chrome extension. The extension communicates with desktop consumers through a WebSocket bridge at ws://127.0.0.1:9339/slop-bridge.
Browser providers have two transport types:
| Transport | How it works | Consumer connects via |
|---|---|---|
ws (server-backed) | Tab has its own WebSocket server | Direct WebSocket (no relay needed) |
postmessage (SPA) | Tab uses window.postMessage | Relay through the extension bridge |
The discovery layer merges local and bridge providers into a single list. Consumers see a unified ProviderDescriptor[] regardless of source.
Extension Bridge
Section titled “Extension Bridge”Protocol
Section titled “Protocol”The bridge is a WebSocket server that acts as a message hub between the browser extension and desktop consumers.
Extension → Bridge:
| Message | Purpose |
|---|---|
provider-available | Announce a discovered browser tab provider |
provider-unavailable | Tab closed or provider removed |
slop-relay | Forward a SLOP message from a postMessage provider |
Consumer → Bridge:
| Message | Purpose |
|---|---|
relay-open | Start relaying for a specific provider |
relay-close | Stop relaying |
slop-relay | Forward a SLOP message to a postMessage provider |
The bridge rebroadcasts all messages to all connected sinks. When a new client connects, the bridge replays all currently known providers.
Bridge server fallback
Section titled “Bridge server fallback”Only one process can bind to the bridge port (default 9339). The discovery layer uses a “try client first, fall back to server” strategy:
1. Try connecting as a bridge client to ws://127.0.0.1:9339/slop-bridge2. If connection succeeds → use client mode (Desktop or another consumer hosts the bridge)3. If connection fails → start a bridge server on port 93394. If server bind fails (port race) → retry as clientThis means:
- If the Desktop app is running, all other consumers connect as clients to its bridge
- If the Desktop app is NOT running, the first consumer to start becomes the bridge host
- Subsequent consumers connect as clients to whichever consumer started the bridge
- No separate daemon or installation required
The bridge server implementation must:
- Accept WebSocket connections on the configured port and path
- Store provider announcements and replay them to new connections
- Forward all message types (
slop-relay,relay-open,relay-close,provider-available,provider-unavailable) to all connected sinks - Track relay subscriptions per provider key for internal dispatch
- Clean up relay subscriptions when providers go unavailable or connections close
Relay transport
Section titled “Relay transport”For postmessage providers, the discovery layer provides a relay transport that wraps the bridge connection as a standard ClientTransport:
- Send
relay-opento the bridge (extension activates content script relay) - Wait for the extension to activate (the content script needs to add its
window.addEventListener) - Send SLOP
connecthandshake through the relay - Provider responds with
hellothrough the relay - All subsequent SLOP messages flow through
slop-relaywrappers
The relay transport implements the same ClientTransport interface as WebSocket or Unix socket transports. The SlopConsumer doesn’t know it’s talking through a relay — the transport is pluggable.
Connection Management
Section titled “Connection Management”Lazy vs auto-connect
Section titled “Lazy vs auto-connect”The discovery layer supports two modes:
-
Lazy connect (default): Providers are discovered but not connected until explicitly requested via
ensureConnected(idOrName). Good for interactive tools where the user chooses which app to connect to. -
Auto-connect: All discovered providers are connected immediately on discovery. Good for background services (like an AI tool plugin) that need state available before the user asks.
Idle timeout
Section titled “Idle timeout”Connected providers are disconnected after 5 minutes of inactivity to free resources. The timeout resets on any access (getProvider, ensureConnected, tool invocation).
Reconnection
Section titled “Reconnection”When a connected provider disconnects unexpectedly and its descriptor still exists, the discovery layer reconnects with exponential backoff:
- Initial delay: 3 seconds
- Backoff multiplier: 2x
- Maximum delay: 30 seconds
- Resets on successful reconnection
Connection timeout
Section titled “Connection timeout”Connection attempts time out after 10 seconds. This prevents the discovery layer from hanging indefinitely on unresponsive providers (e.g., a descriptor file exists but the process isn’t running).
State Formatting
Section titled “State Formatting”The discovery layer provides two functions for formatting provider state for AI consumption:
formatTree(node)
Section titled “formatTree(node)”Renders a state tree as a human-readable string:
[root] my-app: My Application [collection] items (total=3) [item] item-1: First Item (status="active") actions: {edit(title: string), delete} [item] item-2: Second Item (status="done") actions: {edit(title: string), delete} [view] settings actions: {toggle_theme, export_data}Includes node types, IDs, properties, affordances with parameter types, salience scores, and windowing indicators.
affordancesToTools(node)
Section titled “affordancesToTools(node)”Converts all affordances in a state tree into LLM tool definitions:
- Tool names use
{nodeId}__{action}format (e.g.,item_1__edit) - Collisions are disambiguated by prepending ancestor IDs
- Returns a
resolve(toolName)function that maps tool names back to{ path, action }forinvoke - Tool descriptions include the node path, action label, and
[DANGEROUS]flag
createDynamicTools(discovery)
Section titled “createDynamicTools(discovery)”Builds namespaced tool definitions from all connected providers’ affordances. Each tool name is prefixed with the provider’s ID to avoid cross-app collisions:
kanban__backlog__add_card → invoke("/columns/backlog", "add_card", ...)kanban__col_1__move_card → invoke("/columns/col-1", "move_card", ...)chat__messages__send → invoke("/messages", "send", ...)Returns a DynamicToolSet with:
tools— array ofDynamicToolEntryobjects (name, description, inputSchema, providerId, path, action)resolve(toolName)— maps a dynamic tool name back to{ providerId, path, action }for dispatch
This function is called on every state change to rebuild the tool list. Integrations that support dynamic tool registration (like MCP’s notifications/tools/list_changed) use this to expose affordances as first-class tools. See Dynamic tool injection below.
State Change Notifications
Section titled “State Change Notifications”The discovery layer fires a state change callback on:
- Provider connected
- Provider disconnected
- State patch received from any connected provider
Consumers can use this to maintain a cache, update a UI, or trigger context injection. The callback receives no arguments — the consumer reads the current state from the discovery service.
SDK Implementations
Section titled “SDK Implementations”| Language | Consumer SDK | Core discovery module | Current status |
|---|---|---|---|
| TypeScript | @slop-ai/consumer | @slop-ai/discovery | Reference implementation for phase 1, plus host-specific helpers outside the shared contract |
| Python | slop-ai | slop_ai.discovery | Initial phase-1 implementation shipped in the SDK and normalized to the shared contract |
| Go | slop-ai | github.com/devteapot/slop/packages/go/slop-ai/discovery | Initial phase-1 implementation shipped in the SDK and normalized to the shared contract |
| Rust | slop-ai | slop_ai::discovery | Initial phase-1 implementation shipped in the SDK and normalized to the shared contract |
Phase 1 capability matrix
Section titled “Phase 1 capability matrix”| Capability | TypeScript | Python | Go | Rust |
|---|---|---|---|---|
| Local provider scanning | Shipped | Shipped | Shipped | Shipped |
| Descriptor validation and pruning | Shipped | Shipped | Shipped | Shipped |
| Directory watch + 15s fallback scan | Shipped | Shipped | Shipped | Shipped |
| Bridge client | Shipped | Shipped | Shipped | Shipped |
| Bridge server | Shipped | Shipped | Shipped | Shipped |
| Client-first / server-fallback startup | Shipped | Shipped | Shipped | Shipped |
| Relay transport | Shipped | Shipped | Shipped | Shipped |
| Discovery service / connection orchestration | Shipped | Shipped | Shipped | Shipped |
Lazy connect + ensureConnected() | Shipped | Shipped | Shipped | Shipped |
| Auto-connect mode | Shipped | Shipped | Shipped | Shipped |
| Idle timeout | Shipped | Shipped | Shipped | Shipped |
| Reconnect backoff | Shipped | Shipped | Shipped | Shipped |
| Connection timeout | Shipped | Shipped | Shipped | Shipped |
| State change callback | Shipped | Shipped | Shipped | Shipped |
formatTree() | Shipped | Shipped | Shipped | Shipped |
affordancesToTools() | Shipped | Shipped | Shipped | Shipped |
createToolHandlers() | Shipped | Shipped | Shipped | Shipped |
createDynamicTools() | Shipped | Shipped | Shipped | Shipped |
| Host wrappers and prompt injection glue | Shipped | Out of scope | Out of scope | Out of scope |
| State cache / shared file helpers | Shipped | Out of scope | Out of scope | Out of scope |
Known donor-code drift
Section titled “Known donor-code drift”The Go CLI and Rust Desktop codebases are useful donor implementations, but they are not the behavioral contract. New SDK modules should match the TypeScript phase-1 semantics above, even when that means changing extracted code.
Go donor drift (apps/cli)
Section titled “Go donor drift (apps/cli)”apps/cli/bridge/server.goforwardsprovider-available,provider-unavailable, andslop-relay, but does not yet forwardrelay-openandrelay-closeapps/cli/bridge/client.godoes not maintain a reconnect loop after bridge disconnectsapps/cli/main.gotries client first, but on bind failure it disables the bridge instead of retrying as a client after a port raceapps/cli/bridge/relay.goopens the relay and returns immediately; it does not yet match the TypeScript relay handshake retry and early-message buffering behaviorapps/cli/tui/discovery.gorefreshes via TUI polling instead of a reusable discovery service with directory watchers, pruning, idle timeout, and reconnect backoffapps/cli/provider/discovery.gofilters dead PIDs today; extraction should keep only behavior that is explicitly part of the shared contract
Rust donor drift (apps/desktop/src-tauri)
Section titled “Rust donor drift (apps/desktop/src-tauri)”apps/desktop/src-tauri/src/bridge/mod.rsis coupled totauri::AppHandle, app events, and desktop registry state, so it cannot be extracted as-is into the SDKapps/desktop/src-tauri/src/provider/mod.rssendsrelay-openand a singleconnect, but does not yet match the TypeScript relay handshake retry and early-message buffering behaviorapps/desktop/src-tauri/src/provider/discovery.rsonly scans descriptor files; it does not yet implement the full watcher, fallback-rescan, validation, and pruning behaviorapps/desktop/src-tauri/src/provider/mod.rsingests discovered and bridge providers additively; extraction should also handle descriptor disappearance and bridge provider removal using the shared contract- The desktop app currently hosts a bridge server, but it does not provide a reusable bridge client or the full client-first / server-fallback startup flow
Implementation checklist for new SDKs
Section titled “Implementation checklist for new SDKs”A complete phase-1 discovery layer implementation provides:
- Local provider scanning (
~/.slop/providers/+/tmp/slop/providers/) - Directory watching with periodic fallback scan
- Bridge client (connect to existing bridge)
- Bridge server (host bridge if none exists)
- “Try client, fall back to server” startup
- Relay transport for postMessage providers
-
formatTree()for LLM context -
affordancesToTools()for LLM tool generation - Auto-connect mode
- Lazy connect with
ensureConnected() - Idle timeout (5 minutes default)
- Exponential backoff reconnection
- Connection timeout (10 seconds)
- State change callback
- Dynamic tool generation (
createDynamicTools()equivalent)
Host-specific wrappers and prompt injection are intentionally outside phase 1.
Integrations
Section titled “Integrations”Both the Claude Code and OpenClaw plugins follow the same design principles:
- State injection — Provider state is injected into the model’s context before each turn, not fetched via tool calls
- Minimal tool usage — Tools are used only for connecting to apps and performing actions, never for reading state
- Shared discovery — Both import
@slop-ai/discoveryfor provider scanning, bridge, and relay
Where they differ is action dispatch, due to host platform limitations.
Dynamic tool injection
Section titled “Dynamic tool injection”When a host supports runtime tool registration, the discovery layer can expose each affordance as a first-class tool. createDynamicTools(discovery) generates namespaced tool definitions from all connected providers:
kanban__backlog__add_card({title: "Ship docs"}) ← model calls this directlyInstead of:
app_action(app="kanban", path="/columns/backlog", action="add_card", params={title: "Ship docs"})Dynamic tools have proper parameter schemas from the provider’s affordance definitions. They are rebuilt on every state change (affordance added/removed, provider connect/disconnect).
Host support:
| Host | Dynamic tools | Mechanism | Limitation |
|---|---|---|---|
| Codex | No (current plugin) | Stable MCP tools + UserPromptSubmit hook-based state injection | No runtime tool registration; actions still go through meta-tools |
| Claude Code (MCP) | Yes | notifications/tools/list_changed — server notifies client when tool list changes | None |
| OpenClaw | No | api.registerTool() is one-time during register() | No runtime tool registration API; tools must be declared in the plugin manifest |
Hosts without dynamic tool support fall back to the meta-tool pattern: stable tools (app_action, app_action_batch) that resolve actions at runtime. Depending on the host, the model learns the exact paths and action names from prompt-time state injection or from an explicit connect_app inspection step.
Codex plugin (packages/typescript/integrations/codex/slop)
Section titled “Codex plugin (packages/typescript/integrations/codex/slop)”| Component | Purpose |
|---|---|
| Tools | list_apps, connect_app, disconnect_app, app_action, app_action_batch |
Hook (UserPromptSubmit) | Reads a shared state file and injects connected providers’ trees into Codex on every user message |
Skill (slop-connect) | Teaches Codex the connect-once → inspect → act workflow |
Design details:
- Fixed tool surface — The Codex plugin exposes the same stable five-tool catalog as the OpenClaw plugin.
- Hook-based state injection — The bridge writes provider state to
/tmp/codex-slop-plugin/state.jsonon every state change. TheUserPromptSubmithook reads that file and injects fresh markdown into future turns. - Immediate snapshot on connect —
connect_appstill returns the current tree and actions right away, so Codex can act in the same turn it first connects. - Discovery — Uses
@slop-ai/discoverywith local descriptor watching plus browser bridge support. - Multi-app — Multiple providers can remain connected concurrently;
app_actionandapp_action_batchresolve against the requested app ID.
See Codex guide for setup and usage.
Claude Code integrations (claude-slop-native, claude-slop-mcp-proxy)
Section titled “Claude Code integrations (claude-slop-native, claude-slop-mcp-proxy)”| Variant | Purpose |
|---|---|
claude-slop-native | Wraps createDiscoveryService + createDynamicTools from @slop-ai/discovery. Registers dynamic per-app tools via tools/list_changed. Static tools: list_apps, connect_app, disconnect_app. |
claude-slop-mcp-proxy | Wraps createDiscoveryService from @slop-ai/discovery, but keeps a fixed tool catalog: list_apps, connect_app, disconnect_app, app_action, app_action_batch. |
Shared hook (UserPromptSubmit) | Reads a shared state file and injects connected providers’ state trees into Claude’s context on every user message — no MCP fetch needed. Also lists discovered-but-not-connected apps. |
Shared skill (slop-connect) | Teaches Claude the list → connect → inspect → act workflow. |
Design details:
- Native direct tools — When
connect_app("kanban")connects a provider,claude-slop-nativeregisters affordances as MCP tools (e.g.,kanban__add_card). Claude calls them directly. When the provider disconnects, the tools are removed. - MCP proxy fallback —
claude-slop-mcp-proxydoes not register dynamic tools. Instead, Claude reads state from context and callsapp_action(app, path, action, params)orapp_action_batch(...). - Live state in context — Both variants write provider state to
/tmp/claude-slop-plugin/state.jsonon every state change. The hook reads this file and outputs markdown that Claude sees on every turn. - Staleness protection — The state file includes a
lastUpdatedtimestamp. The hook skips injection if the file is older than 30 seconds. - Multi-app — Multiple providers can be connected simultaneously. In the native variant, dynamic tools from different apps are distinguished by their app ID prefix.
See Claude Code guide for setup and usage.
OpenClaw plugin (@slop-ai/openclaw-plugin)
Section titled “OpenClaw plugin (@slop-ai/openclaw-plugin)”| Component | Purpose |
|---|---|
| Tools | list_apps (list), connect_app (connect/inspect), disconnect_app, app_action (single action), app_action_batch (bulk ops) — registered once during register() |
Hook (before_prompt_build) | Injects connected providers’ state trees as prependContext on every inference turn |
Design details:
- Meta-tool pattern — OpenClaw’s plugin SDK requires tools to be declared upfront in
openclaw.plugin.jsonand registered once. Dynamic tool registration is not supported. Actions go throughapp_action(app, path, action, params)instead of per-app tools. - State injection — The
before_prompt_buildhook returns{ prependContext: stateMarkdown }, which OpenClaw prepends to the conversation before inference. No file-based IPC needed (in-process). - Discovery — Uses
@slop-ai/discoverywith bridge support. Discovers local providers, session providers, and browser tabs via extension bridge.
See OpenClaw guide for setup and usage.
Related
Section titled “Related”- Consumer SDK API — protocol client reference
- Transport spec — wire protocol and discovery mechanisms
- Adapters spec — bridging non-SLOP apps
- Consumer guide — usage patterns and example workflows
- Codex guide — Codex plugin setup and usage
- Claude Code guide — Claude Code plugin setup and usage