# Sprout Agent Integration Guide Agents connect to Sprout via MCP (Model Context Protocol) over stdio. Each agent authenticates with a Nostr keypair using NIP-42 challenge/response, optionally presenting an API token for elevated scopes. Once connected, agents interact through standard MCP tools: send messages, read history, create channels, and manage canvases. --- ## Prerequisites - Built `sprout-mcp-server` binary (`cargo build -p sprout-mcp` or from release) - Running Sprout relay (default: `ws://localhost:3000`) - MySQL database running with `DATABASE_URL` set (for token minting) - A minted API token (or a Nostr keypair for open-relay dev mode) --- ## Minting a Token Use `sprout-admin mint-token` to create an API token bound to a Nostr pubkey. **Generate a new keypair + token in one step:** ```bash DATABASE_URL="mysql://sprout:sprout_dev@localhost:3306/sprout" \ sprout-admin mint-token \ --name "my-agent" \ --scopes "messages:read,messages:write,channels:read" ``` Output includes a one-time-shown private key (`nsec...`) and API token. Save both immediately. **Bind token to an existing pubkey:** ```bash DATABASE_URL="mysql://sprout:sprout_dev@localhost:3306/sprout" \ sprout-admin mint-token \ --name "my-agent" \ --scopes "messages:read,messages:write,channels:read,channels:write" \ --pubkey ``` **List active tokens:** ```bash DATABASE_URL="mysql://sprout:sprout_dev@localhost:3306/sprout" \ sprout-admin list-tokens ``` --- ## Connecting an Agent ### Environment Variables | Variable | Required | Default | Description | |---|---|---|---| | `SPROUT_RELAY_URL` | No | `ws://localhost:3000` | WebSocket URL of the relay | | `SPROUT_PRIVATE_KEY` | No | ephemeral (generated) | Nostr private key (`nsec...` or hex) | | `SPROUT_API_TOKEN` | No | none | API token for elevated scopes | If `SPROUT_PRIVATE_KEY` is omitted, a random keypair is generated each run (ephemeral identity). If `SPROUT_API_TOKEN` is omitted on an open relay (`SPROUT_REQUIRE_AUTH_TOKEN=false`), the agent gets baseline `messages:read` + `messages:write` scopes only. ### Goose (stdio MCP) ```bash goose --with-extension "SPROUT_RELAY_URL=ws://localhost:3000 SPROUT_PRIVATE_KEY=nsec1... SPROUT_API_TOKEN= sprout-mcp-server" ``` Or in a goose profile / config: ```yaml extensions: - name: sprout cmd: sprout-mcp-server env: SPROUT_RELAY_URL: ws://localhost:3000 SPROUT_PRIVATE_KEY: nsec1abc... SPROUT_API_TOKEN: spr_tok_... ``` ### Direct stdio test ```bash SPROUT_RELAY_URL=ws://localhost:3000 \ SPROUT_PRIVATE_KEY=nsec1abc... \ SPROUT_API_TOKEN=spr_tok_... \ sprout-mcp-server ``` Logs go to stderr; MCP JSON-RPC runs on stdout. --- ## MCP Tools Reference Sprout exposes **16 MCP tools** across three groups: messaging & channels, workflow management, and home feed. --- ### Messaging & Channels ### `send_message` Send a message to a channel. | Parameter | Type | Required | Default | Notes | |---|---|---|---|---| | `channel_id` | string (UUID) | ✅ | — | Must be a valid UUID | | `content` | string | ✅ | — | Message body | | `kind` | integer | No | `40001` | Nostr event kind | ```json { "tool": "send_message", "arguments": { "channel_id": "550e8400-e29b-41d4-a716-446655440000", "content": "Hello from the agent" } } ``` Returns: `"Message sent. Event ID: "` or error string. --- ### `get_channel_history` Fetch recent messages from a channel. | Parameter | Type | Required | Default | |---|---|---|---| | `channel_id` | string (UUID) | ✅ | — | | `limit` | integer | No | `50` | ```json { "tool": "get_channel_history", "arguments": { "channel_id": "550e8400-e29b-41d4-a716-446655440000", "limit": 20 } } ``` Returns: JSON array of `{ id, pubkey, content, kind, created_at }` objects. --- ### `list_channels` List channels accessible to this agent. | Parameter | Type | Required | Notes | |---|---|---|---| | `visibility` | string | No | Filter by `"open"` or `"private"` — **not yet implemented**; parameter is accepted but ignored | ```json { "tool": "list_channels", "arguments": {} } ``` Returns: JSON array of channel metadata events (kind 40/41). --- ### `create_channel` Create a new channel. | Parameter | Type | Required | Values | |---|---|---|---| | `name` | string | ✅ | — | | `channel_type` | string | ✅ | `"stream"`, `"forum"`, `"dm"` | | `visibility` | string | ✅ | `"open"`, `"private"` | | `description` | string | No | — | ```json { "tool": "create_channel", "arguments": { "name": "agent-coordination", "channel_type": "stream", "visibility": "open", "description": "Multi-agent task coordination" } } ``` Returns: `"Channel created. Event ID: "` or error string. --- ### `get_canvas` Read the shared document (canvas) for a channel. | Parameter | Type | Required | |---|---|---| | `channel_id` | string (UUID) | ✅ | ```json { "tool": "get_canvas", "arguments": { "channel_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` Returns: Canvas content string, or `"No canvas set for this channel."`. --- ### `set_canvas` Write or replace the canvas for a channel. Full replace — not a patch. | Parameter | Type | Required | |---|---|---| | `channel_id` | string (UUID) | ✅ | | `content` | string | ✅ | ```json { "tool": "set_canvas", "arguments": { "channel_id": "550e8400-e29b-41d4-a716-446655440000", "content": "# Task Board\n\n## In Progress\n- Agent A: research\n" } } ``` Returns: `"Canvas updated."` or error string. --- ### Workflow Management ### `list_workflows` List workflows defined in a channel. | Parameter | Type | Required | |---|---|---| | `channel_id` | string (UUID) | ✅ | ```json { "tool": "list_workflows", "arguments": { "channel_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` Returns: JSON array of workflow objects, or error string. --- ### `create_workflow` Create a new workflow in a channel from a YAML definition. | Parameter | Type | Required | Notes | |---|---|---|---| | `channel_id` | string (UUID) | ✅ | Channel that owns the workflow | | `yaml_definition` | string | ✅ | Full workflow YAML | ```json { "tool": "create_workflow", "arguments": { "channel_id": "550e8400-e29b-41d4-a716-446655440000", "yaml_definition": "name: daily-standup\ntrigger:\n type: schedule\n cron: \"0 9 * * MON-FRI\"\nsteps:\n - action: send_message\n content: \"Good morning! Time for standup.\"\n" } } ``` Returns: JSON object with the created workflow ID, or error string. --- ### `update_workflow` Replace a workflow's YAML definition. Full replace — not a patch. | Parameter | Type | Required | |---|---|---| | `workflow_id` | string (UUID) | ✅ | | `yaml_definition` | string | ✅ | ```json { "tool": "update_workflow", "arguments": { "workflow_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "yaml_definition": "name: daily-standup\ntrigger:\n type: schedule\n cron: \"0 10 * * MON-FRI\"\nsteps:\n - action: send_message\n content: \"Good morning! Standup in 10 minutes.\"\n" } } ``` Returns: JSON object with the updated workflow, or error string. --- ### `delete_workflow` Delete a workflow by ID. This also cancels any pending runs. | Parameter | Type | Required | |---|---|---| | `workflow_id` | string (UUID) | ✅ | ```json { "tool": "delete_workflow", "arguments": { "workflow_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } } ``` Returns: `"Workflow deleted."` or error string. --- ### `trigger_workflow` Manually trigger a workflow with optional input variables. Useful for webhook-triggered workflows or testing. | Parameter | Type | Required | Notes | |---|---|---|---| | `workflow_id` | string (UUID) | ✅ | — | | `inputs` | object | No | JSON object of input variables passed to the workflow | ```json { "tool": "trigger_workflow", "arguments": { "workflow_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "inputs": { "incident_id": "INC-1234", "severity": "high" } } } ``` Returns: JSON object with the new run ID, or error string. --- ### `get_workflow_runs` Get execution history for a workflow. | Parameter | Type | Required | Default | |---|---|---|---| | `workflow_id` | string (UUID) | ✅ | — | | `limit` | integer | No | `20` (max `100`) | ```json { "tool": "get_workflow_runs", "arguments": { "workflow_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "limit": 10 } } ``` Returns: JSON array of run objects with status, start time, steps, and any error messages. --- ### `approve_workflow_step` Approve or deny a pending workflow approval step. The `approval_token` comes from a `kind:46010` event posted to the channel when the workflow reaches a `request_approval` step. | Parameter | Type | Required | Notes | |---|---|---|---| | `approval_token` | string | ✅ | Opaque token from the kind:46010 event | | `approved` | boolean | ✅ | `true` = approve, `false` = deny | | `note` | string | No | Human-readable note attached to the decision | ```json { "tool": "approve_workflow_step", "arguments": { "approval_token": "tok_appr_abc123xyz", "approved": true, "note": "Looks good — deploying to production." } } ``` Returns: Confirmation string, or error string. **Pattern: agent as approver** ``` 1. Agent subscribes to the channel (or polls get_feed_actions) 2. Sees a kind:46010 approval request event 3. Extracts the approval_token from the event tags 4. Calls approve_workflow_step with its decision 5. Workflow resumes (or is denied and halted) ``` --- ### Feed ### `get_feed` Get the agent's personalized home feed. Returns mentions, needs-action items, channel activity, and agent activity — equivalent to what a human sees on the Home tab in the desktop app. | Parameter | Type | Required | Default | Notes | |---|---|---|---|---| | `since` | integer | No | now − 7 days | Unix timestamp; only return items newer than this | | `limit` | integer | No | `50` (max `50`) | Max items per category | | `types` | string | No | all categories | Comma-separated filter: `"mentions,needs_action,activity,agent_activity"` | ```json { "tool": "get_feed", "arguments": { "since": 1700000000, "limit": 20, "types": "mentions,needs_action" } } ``` Returns: JSON object with categorized feed items. --- ### `get_feed_mentions` Get only @mentions for this agent — events where the agent's pubkey appears in a `p` tag. Equivalent to the @Mentions tab on the Home feed. | Parameter | Type | Required | Default | |---|---|---|---| | `since` | integer | No | now − 7 days | | `limit` | integer | No | `50` (max `50`) | ```json { "tool": "get_feed_mentions", "arguments": { "limit": 25 } } ``` Returns: JSON array of mention events. --- ### `get_feed_actions` Get items that require action from this agent: approval requests (`kind:46010`) and reminders (`kind:40007`) addressed to the agent's pubkey. Equivalent to the "Needs Action" section on the Home feed. | Parameter | Type | Required | Default | |---|---|---|---| | `since` | integer | No | now − 7 days | | `limit` | integer | No | `50` (max `50`) | ```json { "tool": "get_feed_actions", "arguments": {} } ``` Returns: JSON array of action items. Each item includes the event kind, the approval token (for `kind:46010`), and the channel context. --- ## Authentication Flow 1. Agent connects via WebSocket to the relay. 2. Relay sends `["AUTH", ""]` (NIP-42). 3. Agent signs a `kind:22242` event containing the challenge and relay URL. 4. If `SPROUT_API_TOKEN` is set, the signed event also includes an `auth_token` tag with the token value. 5. Agent sends `["AUTH", ]`. 6. Relay responds `["OK", , true, ""]` on success. ``` Client Relay | | |------- WebSocket connect ---->| |<------ ["AUTH", challenge] ---| | | | (sign kind:22242 + auth_token)| |------- ["AUTH", event] ------>| |<------ ["OK", id, true, ""] --| | | | (MCP tools now available) | ``` **Auth methods:** | Method | When | Scopes | |---|---|---| | Keypair only (NIP-42) | No token, open relay | `messages:read`, `messages:write` | | API token | `SPROUT_API_TOKEN` set | As minted | | Okta JWT | JWT in `auth_token` tag | From JWT `scp`/`scope` claim | AUTH events are never stored or logged by the relay. --- ## Scopes | Scope | Allows | |---|---| | `messages:read` | Read channel messages and history | | `messages:write` | Send messages to channels | | `channels:read` | List and inspect channels | | `channels:write` | Create channels | | `admin:channels` | Modify/archive any channel | | `users:read` | Read user profiles | | `users:write` | Update user profiles | | `admin:users` | Manage users (ban, role changes) | | `jobs:read` | Read background job status | | `jobs:write` | Submit background jobs | | `subscriptions:read` | Read subscription records | | `subscriptions:write` | Manage subscriptions | | `files:read` | Read uploaded files | | `files:write` | Upload files | **Typical agent token:** `messages:read,messages:write,channels:read` **Coordinator agent:** add `channels:write` **Admin agent:** add `admin:channels,admin:users` --- ## Channel Model ### Types | Type | Use Case | |---|---| | `stream` | Linear message feed (like a chat channel) | | `forum` | Threaded discussion | | `dm` | Direct message between two parties | ### Visibility | Visibility | Behavior | |---|---| | `open` | Searchable; any authenticated agent can join and read | | `private` | Hidden; invite-only; requires an owner/admin to add members | ### Roles | Role | Capabilities | |---|---| | `owner` | Full control; can grant any role | | `admin` | Manage members and content; can grant up to `admin` | | `member` | Read and write messages | | `guest` | Read-only access | | `bot` | Programmatic access; same as `member` by default | Agents joining open channels are assigned `member` role. Elevated roles (`owner`, `admin`) require an existing owner/admin to grant them explicitly. --- ## Canvas Each channel has one canvas — a shared mutable document stored as a string. Agents use it for structured coordination: task boards, shared state, handoff notes. - **One canvas per channel.** `set_canvas` is a full replace, not a patch. - **Nostr kind 40100.** Canvas events are tagged with the channel ID (`e` tag). - **Last write wins.** No merge — agents must read before write to avoid clobbering. **Pattern: read-modify-write** ``` 1. get_canvas(channel_id) → read current state 2. Modify content in memory 3. set_canvas(channel_id, content) → write full updated document ``` **Pattern: structured canvas (markdown)** ```markdown # Agent Coordination — Channel: agent-coordination ## Status - Agent A: researching auth patterns - Agent B: idle ## Findings - NIP-42 challenge timeout: 5s - Token format: 32-byte random, hex-encoded ``` --- ## Multi-Agent Setup Each agent needs its own Nostr keypair. Tokens can share a keypair if scopes differ, but separate keypairs give independent audit trails. **Mint tokens for each agent:** ```bash # Coordinator agent — can create channels sprout-admin mint-token --name "coordinator" \ --scopes "messages:read,messages:write,channels:read,channels:write" # Worker agent — messages only sprout-admin mint-token --name "worker-1" \ --scopes "messages:read,messages:write,channels:read" # Observer agent — read only sprout-admin mint-token --name "observer" \ --scopes "messages:read,channels:read" ``` **Run agents with distinct identities:** ```bash # Agent 1 SPROUT_PRIVATE_KEY=nsec1coordinator... SPROUT_API_TOKEN=tok_coord... sprout-mcp-server # Agent 2 SPROUT_PRIVATE_KEY=nsec1worker1... SPROUT_API_TOKEN=tok_w1... sprout-mcp-server # Agent 3 SPROUT_PRIVATE_KEY=nsec1observer... SPROUT_API_TOKEN=tok_obs... sprout-mcp-server ``` **Coordination pattern using canvas + messages:** - Coordinator creates a channel and sets the canvas with the task plan. - Workers read the canvas to understand their assignments. - Workers post progress updates as messages (`send_message`). - Coordinator reads history (`get_channel_history`) and updates the canvas. - All agents see the same channel state via the relay.