@octavus/docs 3.3.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/02-server-sdk/02-sessions.md +61 -0
- package/content/02-server-sdk/03-tools.md +8 -7
- package/content/02-server-sdk/09-inline-mcp.md +204 -0
- package/content/04-protocol/13-mcp-servers.md +73 -14
- package/dist/{chunk-WYXUBTV7.js → chunk-VGQQ3XM5.js} +37 -19
- package/dist/chunk-VGQQ3XM5.js.map +1 -0
- package/dist/content.js +1 -1
- package/dist/docs.json +18 -9
- package/dist/index.js +1 -1
- package/dist/search-index.json +1 -1
- package/dist/search.js +1 -1
- package/dist/search.js.map +1 -1
- package/dist/sections.json +18 -9
- package/package.json +1 -1
- package/dist/chunk-WYXUBTV7.js.map +0 -1
|
@@ -23,7 +23,7 @@ var docs_default = [
|
|
|
23
23
|
section: "server-sdk",
|
|
24
24
|
title: "Overview",
|
|
25
25
|
description: "Introduction to the Octavus Server SDK for backend integration.",
|
|
26
|
-
content: "\n# Server SDK Overview\n\nThe `@octavus/server-sdk` package provides a Node.js SDK for integrating Octavus agents into your backend application. It handles session management, streaming, and the tool execution continuation loop.\n\n**Current version:** `3.
|
|
26
|
+
content: "\n# Server SDK Overview\n\nThe `@octavus/server-sdk` package provides a Node.js SDK for integrating Octavus agents into your backend application. It handles session management, streaming, and the tool execution continuation loop.\n\n**Current version:** `3.5.0`\n\n## Installation\n\n```bash\nnpm install @octavus/server-sdk\n```\n\nFor agent management (sync, validate), install the CLI as a dev dependency:\n\n```bash\nnpm install --save-dev @octavus/cli\n```\n\n## Basic Usage\n\n```typescript\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: 'https://octavus.ai',\n apiKey: 'your-api-key',\n});\n```\n\n## Key Features\n\n### Agent Management\n\nAgent definitions are managed via the CLI. See the [CLI documentation](/docs/server-sdk/cli) for details.\n\n```bash\n# Sync agent from local files\noctavus sync ./agents/support-chat\n\n# Output: Created: support-chat\n# Agent ID: clxyz123abc456\n```\n\n### Session Management\n\nCreate and manage agent sessions using the agent ID:\n\n```typescript\n// Create a new session (use agent ID from CLI sync)\nconst sessionId = await client.agentSessions.create('clxyz123abc456', {\n COMPANY_NAME: 'Acme Corp',\n PRODUCT_NAME: 'Widget Pro',\n});\n\n// Get UI-ready session messages (for session restore)\nconst session = await client.agentSessions.getMessages(sessionId);\n```\n\n### Tool Handlers\n\nTools run on your server with your data:\n\n```typescript\nconst session = client.agentSessions.attach(sessionId, {\n tools: {\n 'get-user-account': async (args) => {\n // Access your database, APIs, etc.\n return await db.users.findById(args.userId);\n },\n },\n});\n```\n\n### Streaming\n\nAll responses stream in real-time:\n\n```typescript\nimport { toSSEStream } from '@octavus/server-sdk';\n\n// execute() returns an async generator of events\nconst events = session.execute({\n type: 'trigger',\n triggerName: 'user-message',\n input: { USER_MESSAGE: 'Hello!' },\n});\n\n// Convert to SSE stream for HTTP responses\nreturn new Response(toSSEStream(events), {\n headers: { 'Content-Type': 'text/event-stream' },\n});\n```\n\n### Computer Capabilities\n\nGive agents access to browser, filesystem, and shell via MCP:\n\n```typescript\nimport { Computer } from '@octavus/computer';\n\nconst computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', ['--browser-url=...']),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', [dir]),\n shell: Computer.shell({ cwd: dir, mode: 'unrestricted' }),\n },\n});\n\nawait computer.start();\n\nconst session = client.agentSessions.attach(sessionId, {\n tools: {\n 'set-chat-title': async (args) => ({ title: args.title }),\n },\n});\n\nsession.setDynamicTools(computer);\n```\n\n### Workers\n\nExecute worker agents for task-based processing:\n\n```typescript\n// Non-streaming: get the output directly\nconst { output } = await client.workers.generate(agentId, {\n TOPIC: 'AI safety',\n});\n\n// Streaming: observe events in real-time\nfor await (const event of client.workers.execute(agentId, input)) {\n // Handle stream events\n}\n```\n\n## API Reference\n\n### OctavusClient\n\nThe main entry point for interacting with Octavus.\n\n```typescript\ninterface OctavusClientConfig {\n baseUrl: string; // Octavus API URL\n apiKey?: string; // Your API key\n traceModelRequests?: boolean; // Enable model request tracing (default: false)\n}\n\nclass OctavusClient {\n readonly agents: AgentsApi;\n readonly agentSessions: AgentSessionsApi;\n readonly workers: WorkersApi;\n readonly files: FilesApi;\n\n constructor(config: OctavusClientConfig);\n}\n```\n\n### AgentSessionsApi\n\nManages agent sessions.\n\n```typescript\nclass AgentSessionsApi {\n // Create a new session\n async create(agentId: string, input?: Record<string, unknown>): Promise<string>;\n\n // Get full session state (for debugging/internal use)\n async get(sessionId: string): Promise<SessionState>;\n\n // Get UI-ready messages (for client display)\n async getMessages(sessionId: string): Promise<UISessionState>;\n\n // Attach to a session for triggering\n attach(sessionId: string, options?: SessionAttachOptions): AgentSession;\n}\n\n// Full session state (internal format)\ninterface SessionState {\n id: string;\n agentId: string;\n input: Record<string, unknown>;\n variables: Record<string, unknown>;\n resources: Record<string, unknown>;\n messages: ChatMessage[]; // Internal message format\n createdAt: string;\n updatedAt: string;\n}\n\n// UI-ready session state\ninterface UISessionState {\n sessionId: string;\n agentId: string;\n messages: UIMessage[]; // UI-ready messages for frontend\n}\n```\n\n### AgentSession\n\nHandles request execution and streaming for a specific session.\n\n```typescript\nclass AgentSession {\n // Execute a request and stream parsed events\n execute(request: SessionRequest, options?: TriggerOptions): AsyncGenerator<StreamEvent>;\n\n // Get the session ID\n getSessionId(): string;\n\n // Register dynamic tools (e.g., pass a Computer or explicit DynamicTool[])\n setDynamicTools(source: ToolProvider | DynamicTool[]): void;\n}\n\ntype SessionRequest = TriggerRequest | ContinueRequest;\n\ninterface TriggerRequest {\n type: 'trigger';\n triggerName: string;\n input?: Record<string, unknown>;\n}\n\ninterface ContinueRequest {\n type: 'continue';\n executionId: string;\n toolResults: ToolResult[];\n}\n\n// Helper to convert events to SSE stream\nfunction toSSEStream(events: AsyncIterable<StreamEvent>): ReadableStream<Uint8Array>;\n```\n\n### FilesApi\n\nHandles file uploads for sessions.\n\n```typescript\nclass FilesApi {\n // Get presigned URLs for file uploads\n async getUploadUrls(sessionId: string, files: FileUploadRequest[]): Promise<UploadUrlsResponse>;\n}\n\ninterface FileUploadRequest {\n filename: string;\n mediaType: string;\n size: number;\n}\n\ninterface UploadUrlsResponse {\n files: {\n id: string; // File ID for references\n uploadUrl: string; // PUT to this URL\n downloadUrl: string; // GET URL after upload\n }[];\n}\n```\n\nThe client uploads files directly to S3 using the presigned upload URL. See [File Uploads](/docs/client-sdk/file-uploads) for the full integration pattern.\n\n## Next Steps\n\n- [Sessions](/docs/server-sdk/sessions) - Deep dive into session management\n- [Tools](/docs/server-sdk/tools) - Implementing tool handlers\n- [Streaming](/docs/server-sdk/streaming) - Understanding stream events\n- [Workers](/docs/server-sdk/workers) - Executing worker agents\n- [Debugging](/docs/server-sdk/debugging) - Model request tracing and debugging\n- [Computer](/docs/server-sdk/computer) - Browser, filesystem, and shell via MCP\n",
|
|
27
27
|
excerpt: "Server SDK Overview The package provides a Node.js SDK for integrating Octavus agents into your backend application. It handles session management, streaming, and the tool execution continuation...",
|
|
28
28
|
order: 1
|
|
29
29
|
},
|
|
@@ -32,7 +32,7 @@ var docs_default = [
|
|
|
32
32
|
section: "server-sdk",
|
|
33
33
|
title: "Sessions",
|
|
34
34
|
description: "Managing agent sessions with the Server SDK.",
|
|
35
|
-
content: "\n# Sessions\n\nSessions represent conversations with an agent. They store conversation history, track resources and variables, and enable stateful interactions.\n\n## Creating Sessions\n\nCreate a session by specifying the agent ID and initial input variables:\n\n```typescript\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n});\n\n// Create a session with the support-chat agent\nconst sessionId = await client.agentSessions.create('support-chat', {\n COMPANY_NAME: 'Acme Corp',\n PRODUCT_NAME: 'Widget Pro',\n USER_ID: 'user-123', // Optional inputs\n});\n\nconsole.log('Session created:', sessionId);\n```\n\n## Getting Session Messages\n\nTo restore a conversation on page load, use `getMessages()` to retrieve UI-ready messages:\n\n```typescript\nconst session = await client.agentSessions.getMessages(sessionId);\n\nconsole.log({\n sessionId: session.sessionId,\n agentId: session.agentId,\n messages: session.messages.length, // UIMessage[] ready for frontend\n});\n```\n\nThe returned messages can be passed directly to the client SDK's `initialMessages` option.\n\n### UISessionState Interface\n\n```typescript\ninterface UISessionState {\n sessionId: string;\n agentId: string;\n messages: UIMessage[]; // UI-ready conversation history\n}\n```\n\n## Full Session State (Debug)\n\nFor debugging or internal use, you can retrieve the complete session state including all variables and internal message format:\n\n```typescript\nconst state = await client.agentSessions.get(sessionId);\n\nconsole.log({\n id: state.id,\n agentId: state.agentId,\n messages: state.messages.length, // ChatMessage[] (internal format)\n resources: state.resources,\n variables: state.variables,\n createdAt: state.createdAt,\n updatedAt: state.updatedAt,\n});\n```\n\n> **Note**: Use `getMessages()` for client-facing code. The `get()` method returns internal message format that includes hidden content not intended for end users.\n\n## Attaching to Sessions\n\nTo trigger actions on a session, you need to attach to it first:\n\n```typescript\nconst session = client.agentSessions.attach(sessionId, {\n tools: {\n // Tool handlers (see Tools documentation)\n },\n resources: [\n // Resource watchers (optional)\n ],\n});\n```\n\n### Attach Options\n\n| Option | Type | Description |\n| ----------------------- | --------------------------------- | ------------------------------------------------------------------------------- |\n| `tools` | `ToolHandlers` | Server-side tool handler functions |\n| `resources` | `Resource[]` | Resource watchers for real-time updates |\n| `onToolResults` | `(results: ToolResult[]) => void` | Callback invoked after server-side tool results are produced |\n| `rejectClientToolCalls` | `boolean` | If `true`, reject tool calls that have no server handler (no client forwarding) |\n\nFor MCP tool integration (browser, filesystem, shell via `@octavus/computer`), register dynamic tools after attaching with `session.setDynamicTools()`. See [Computer](/docs/server-sdk/computer) for details.\n\n## Executing Requests\n\nOnce attached, execute requests on the session using `execute()`:\n\n```typescript\nimport { toSSEStream } from '@octavus/server-sdk';\n\n// execute() handles both triggers and client tool continuations\nconst events = session.execute(\n { type: 'trigger', triggerName: 'user-message', input: { USER_MESSAGE: 'Hello!' } },\n { signal: request.signal },\n);\n\n// Convert to SSE stream for HTTP responses\nreturn new Response(toSSEStream(events), {\n headers: { 'Content-Type': 'text/event-stream' },\n});\n```\n\n### Request Types\n\nThe `execute()` method accepts a discriminated union:\n\n```typescript\ntype SessionRequest = TriggerRequest | ContinueRequest;\n\n// Start a new conversation turn\ninterface TriggerRequest {\n type: 'trigger';\n triggerName: string;\n input?: Record<string, unknown>;\n rollbackAfterMessageId?: string | null; // For retry: truncate messages after this ID\n}\n\n// Continue after client-side tool handling\ninterface ContinueRequest {\n type: 'continue';\n executionId: string;\n toolResults: ToolResult[];\n}\n```\n\nThis makes it easy to pass requests through from the client:\n\n```typescript\n// Simple passthrough from HTTP request body\nexport async function POST(request: Request) {\n const body = await request.json();\n const { sessionId, ...payload } = body;\n\n const session = client.agentSessions.attach(sessionId, {\n tools: {\n /* ... */\n },\n });\n const events = session.execute(payload, { signal: request.signal });\n\n return new Response(toSSEStream(events));\n}\n```\n\n### Stop Support\n\nPass an abort signal to allow clients to stop generation:\n\n```typescript\nconst events = session.execute(request, {\n signal: request.signal, // Forward the client's abort signal\n});\n```\n\nWhen the client aborts the request, the signal propagates through to the LLM provider, stopping generation immediately. Any partial content is preserved.\n\n## WebSocket Handling\n\nFor WebSocket integrations, use `handleSocketMessage()` which manages abort controller lifecycle internally:\n\n```typescript\nimport type { SocketMessage } from '@octavus/server-sdk';\n\n// In your socket handler\nconn.on('data', async (rawData: string) => {\n const msg = JSON.parse(rawData);\n\n if (msg.type === 'trigger' || msg.type === 'continue' || msg.type === 'stop') {\n await session.handleSocketMessage(msg as SocketMessage, {\n onEvent: (event) => conn.write(JSON.stringify(event)),\n onFinish: async () => {\n // Fetch and persist messages to your database for restoration\n },\n });\n }\n});\n```\n\nThe `handleSocketMessage()` method:\n\n- Handles `trigger`, `continue`, and `stop` messages\n- Automatically aborts previous requests when a new one arrives\n- Streams events via the `onEvent` callback\n- Calls `onFinish` after streaming completes (not called if aborted)\n\nSee [Socket Chat Example](/docs/examples/socket-chat) for a complete implementation.\n\n## Session Lifecycle\n\n```mermaid\nflowchart TD\n A[1. CREATE] --> B[2. ATTACH]\n B --> C[3. TRIGGER]\n C --> C\n C --> D[4. RETRIEVE]\n D --> C\n C --> E[5. EXPIRE]\n C --> G[5b. CLEAR]\n G --> F\n E --> F{6. RESTORE?}\n F -->|Yes| C\n F -->|No| A\n\n A -.- A1[\"`**client.agentSessions.create()**\n Returns sessionId\n Initializes state`\"]\n\n B -.- B1[\"`**client.agentSessions.attach()**\n Configure tool handlers\n Configure resource watchers`\"]\n\n C -.- C1[\"`**session.execute()**\n Execute request\n Stream events\n Update state`\"]\n\n D -.- D1[\"`**client.agentSessions.getMessages()**\n Get UI-ready messages\n Check session status`\"]\n\n E -.- E1[\"`Sessions expire after\n 24 hours (configurable)`\"]\n\n G -.- G1[\"`**client.agentSessions.clear()**\n Programmatically clear state\n Session becomes expired`\"]\n\n F -.- F1[\"`**client.agentSessions.restore()**\n Restore from stored messages\n Or create new session`\"]\n```\n\n## Session Expiration\n\nSessions expire after a period of inactivity (default: 24 hours). When you call `getMessages()` or `get()`, the response includes a `status` field:\n\n```typescript\nconst result = await client.agentSessions.getMessages(sessionId);\n\nif (result.status === 'expired') {\n // Session has expired - restore or create new\n console.log('Session expired:', result.sessionId);\n} else {\n // Session is active\n console.log('Messages:', result.messages.length);\n}\n```\n\n### Response Types\n\n| Status | Type | Description |\n| --------- | --------------------- | ------------------------------------------------------------- |\n| `active` | `UISessionState` | Session is active, includes `messages` array |\n| `expired` | `ExpiredSessionState` | Session expired, includes `sessionId`, `agentId`, `createdAt` |\n\n## Persisting Chat History\n\nTo enable session restoration, store the chat messages in your own database after each interaction:\n\n```typescript\n// After each trigger completes, save messages\nconst result = await client.agentSessions.getMessages(sessionId);\n\nif (result.status === 'active') {\n // Store in your database\n await db.chats.update({\n where: { id: chatId },\n data: {\n sessionId: result.sessionId,\n messages: result.messages, // Store UIMessage[] as JSON\n },\n });\n}\n```\n\n> **Best Practice**: Store the full `UIMessage[]` array. This preserves all message parts (text, tool calls, files, etc.) needed for accurate restoration.\n\n## Restoring Sessions\n\nWhen a user returns to your app:\n\n```typescript\n// 1. Load stored data from your database\nconst chat = await db.chats.findUnique({ where: { id: chatId } });\n\n// 2. Check if session is still active\nconst result = await client.agentSessions.getMessages(chat.sessionId);\n\nif (result.status === 'active') {\n // Session is active - use it directly\n return {\n sessionId: result.sessionId,\n messages: result.messages,\n };\n}\n\n// 3. Session expired - restore from stored messages\nif (chat.messages && chat.messages.length > 0) {\n const restored = await client.agentSessions.restore(\n chat.sessionId,\n chat.messages,\n { COMPANY_NAME: 'Acme Corp' }, // Optional: same input as create()\n );\n\n if (restored.restored) {\n // Session restored successfully\n return {\n sessionId: restored.sessionId,\n messages: chat.messages,\n };\n }\n}\n\n// 4. Cannot restore - create new session\nconst newSessionId = await client.agentSessions.create('support-chat', {\n COMPANY_NAME: 'Acme Corp',\n});\n\nreturn {\n sessionId: newSessionId,\n messages: [],\n};\n```\n\n### Restore Response\n\n```typescript\ninterface RestoreSessionResult {\n sessionId: string;\n restored: boolean; // true if restored, false if session was already active\n}\n```\n\n## Complete Example\n\nHere's a complete session management flow:\n\n```typescript\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n});\n\nasync function getOrCreateSession(chatId: string, agentId: string, input: Record<string, unknown>) {\n // Load existing chat data\n const chat = await db.chats.findUnique({ where: { id: chatId } });\n\n if (chat?.sessionId) {\n // Check session status\n const result = await client.agentSessions.getMessages(chat.sessionId);\n\n if (result.status === 'active') {\n return { sessionId: result.sessionId, messages: result.messages };\n }\n\n // Try to restore expired session\n if (chat.messages?.length > 0) {\n const restored = await client.agentSessions.restore(chat.sessionId, chat.messages, input);\n if (restored.restored) {\n return { sessionId: restored.sessionId, messages: chat.messages };\n }\n }\n }\n\n // Create new session\n const sessionId = await client.agentSessions.create(agentId, input);\n\n // Save to database\n await db.chats.upsert({\n where: { id: chatId },\n create: { id: chatId, sessionId, messages: [] },\n update: { sessionId, messages: [] },\n });\n\n return { sessionId, messages: [] };\n}\n```\n\n## Clearing Sessions\n\nTo programmatically clear a session's state (e.g., for testing reset/restore flows), use `clear()`:\n\n```typescript\nconst result = await client.agentSessions.clear(sessionId);\nconsole.log(result.cleared); // true\n```\n\nAfter clearing, the session transitions to `expired` status. You can then restore it with `restore()` or create a new session.\n\n```typescript\ninterface ClearSessionResult {\n sessionId: string;\n cleared: boolean;\n}\n```\n\nThis is idempotent - calling `clear()` on an already expired session succeeds without error.\n\n## Error Handling\n\n```typescript\nimport { ApiError } from '@octavus/server-sdk';\n\ntry {\n const session = await client.agentSessions.getMessages(sessionId);\n} catch (error) {\n if (error instanceof ApiError) {\n if (error.status === 404) {\n // Session not found or expired\n console.log('Session expired, create a new one');\n } else {\n console.error('API Error:', error.message);\n }\n }\n throw error;\n}\n```\n",
|
|
35
|
+
content: "\n# Sessions\n\nSessions represent conversations with an agent. They store conversation history, track resources and variables, and enable stateful interactions.\n\n## Creating Sessions\n\nCreate a session by specifying the agent ID and initial input variables:\n\n```typescript\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n});\n\n// Create a session with the support-chat agent\nconst sessionId = await client.agentSessions.create('support-chat', {\n COMPANY_NAME: 'Acme Corp',\n PRODUCT_NAME: 'Widget Pro',\n USER_ID: 'user-123', // Optional inputs\n});\n\nconsole.log('Session created:', sessionId);\n```\n\n## Getting Session Messages\n\nTo restore a conversation on page load, use `getMessages()` to retrieve UI-ready messages:\n\n```typescript\nconst session = await client.agentSessions.getMessages(sessionId);\n\nconsole.log({\n sessionId: session.sessionId,\n agentId: session.agentId,\n messages: session.messages.length, // UIMessage[] ready for frontend\n});\n```\n\nThe returned messages can be passed directly to the client SDK's `initialMessages` option.\n\n### UISessionState Interface\n\n```typescript\ninterface UISessionState {\n sessionId: string;\n agentId: string;\n messages: UIMessage[]; // UI-ready conversation history\n}\n```\n\n## Full Session State (Debug)\n\nFor debugging or internal use, you can retrieve the complete session state including all variables and internal message format:\n\n```typescript\nconst state = await client.agentSessions.get(sessionId);\n\nconsole.log({\n id: state.id,\n agentId: state.agentId,\n messages: state.messages.length, // ChatMessage[] (internal format)\n resources: state.resources,\n variables: state.variables,\n createdAt: state.createdAt,\n updatedAt: state.updatedAt,\n});\n```\n\n> **Note**: Use `getMessages()` for client-facing code. The `get()` method returns internal message format that includes hidden content not intended for end users.\n\n## Getting Execution Logs\n\n`getLogs()` returns the chronological execution trace for a session - triggers, messages, tool calls, LLM responses, errors, and other events emitted while the agent ran. Useful for debugging, observability, and building custom timeline views.\n\n```typescript\nconst result = await client.agentSessions.getLogs(sessionId);\n\nif (result.status === 'expired') {\n console.log('Session expired:', result.sessionId);\n} else {\n for (const entry of result.entries) {\n console.log(entry.type, entry.timestamp);\n }\n}\n```\n\nEach entry is a typed variant of `ExecutionLogEntry` (a discriminated union) so consumers can narrow on `entry.type`:\n\n```typescript\nconst result = await client.agentSessions.getLogs(sessionId);\n\nif (result.status !== 'expired') {\n const toolCalls = result.entries.filter((e) => e.type === 'tool-call');\n for (const call of toolCalls) {\n // call.toolName, call.toolArguments are typed without optional chaining\n console.log(call.toolName, call.toolArguments);\n }\n}\n```\n\n### Excluding Model Request Payloads\n\nModel-request entries include the full provider request body and can be large. Pass `excludeModelRequests: true` to skip them:\n\n```typescript\nconst result = await client.agentSessions.getLogs(sessionId, {\n excludeModelRequests: true,\n});\n```\n\n### Truncation\n\nResponses are capped at 1000 entries (most recent). When the log exceeds that cap, the response includes `total` and `truncated` so consumers can detect this:\n\n```typescript\nconst result = await client.agentSessions.getLogs(sessionId);\n\nif (result.status !== 'expired' && result.truncated) {\n console.warn(`Showing latest 1000 of ${result.total} entries`);\n}\n```\n\n### Response Types\n\n| Status | Type | Description |\n| --------- | --------------------- | -------------------------------------------------------------------------------------------- |\n| `active` | `ExecutionLogsResult` | `{ sessionId, entries, total?, truncated? }`. `total` and `truncated` are present when known |\n| `expired` | `ExpiredSessionState` | `{ sessionId, agentId, status: 'expired', createdAt }` |\n\n> **Forward-compatible types**: `ExecutionLogEntry` may gain new variants over time. Include a `default` case when switching on `entry.type` so unknown variants are handled gracefully.\n\n## Attaching to Sessions\n\nTo trigger actions on a session, you need to attach to it first:\n\n```typescript\nconst session = client.agentSessions.attach(sessionId, {\n tools: {\n // Tool handlers (see Tools documentation)\n },\n resources: [\n // Resource watchers (optional)\n ],\n});\n```\n\n### Attach Options\n\n| Option | Type | Description |\n| ----------------------- | --------------------------------- | ------------------------------------------------------------------------------- |\n| `tools` | `ToolHandlers` | Server-side tool handler functions |\n| `resources` | `Resource[]` | Resource watchers for real-time updates |\n| `onToolResults` | `(results: ToolResult[]) => void` | Callback invoked after server-side tool results are produced |\n| `rejectClientToolCalls` | `boolean` | If `true`, reject tool calls that have no server handler (no client forwarding) |\n\nFor MCP tool integration (browser, filesystem, shell via `@octavus/computer`), register dynamic tools after attaching with `session.setDynamicTools()`. See [Computer](/docs/server-sdk/computer) for details.\n\n## Executing Requests\n\nOnce attached, execute requests on the session using `execute()`:\n\n```typescript\nimport { toSSEStream } from '@octavus/server-sdk';\n\n// execute() handles both triggers and client tool continuations\nconst events = session.execute(\n { type: 'trigger', triggerName: 'user-message', input: { USER_MESSAGE: 'Hello!' } },\n { signal: request.signal },\n);\n\n// Convert to SSE stream for HTTP responses\nreturn new Response(toSSEStream(events), {\n headers: { 'Content-Type': 'text/event-stream' },\n});\n```\n\n### Request Types\n\nThe `execute()` method accepts a discriminated union:\n\n```typescript\ntype SessionRequest = TriggerRequest | ContinueRequest;\n\n// Start a new conversation turn\ninterface TriggerRequest {\n type: 'trigger';\n triggerName: string;\n input?: Record<string, unknown>;\n rollbackAfterMessageId?: string | null; // For retry: truncate messages after this ID\n}\n\n// Continue after client-side tool handling\ninterface ContinueRequest {\n type: 'continue';\n executionId: string;\n toolResults: ToolResult[];\n}\n```\n\nThis makes it easy to pass requests through from the client:\n\n```typescript\n// Simple passthrough from HTTP request body\nexport async function POST(request: Request) {\n const body = await request.json();\n const { sessionId, ...payload } = body;\n\n const session = client.agentSessions.attach(sessionId, {\n tools: {\n /* ... */\n },\n });\n const events = session.execute(payload, { signal: request.signal });\n\n return new Response(toSSEStream(events));\n}\n```\n\n### Stop Support\n\nPass an abort signal to allow clients to stop generation:\n\n```typescript\nconst events = session.execute(request, {\n signal: request.signal, // Forward the client's abort signal\n});\n```\n\nWhen the client aborts the request, the signal propagates through to the LLM provider, stopping generation immediately. Any partial content is preserved.\n\n## WebSocket Handling\n\nFor WebSocket integrations, use `handleSocketMessage()` which manages abort controller lifecycle internally:\n\n```typescript\nimport type { SocketMessage } from '@octavus/server-sdk';\n\n// In your socket handler\nconn.on('data', async (rawData: string) => {\n const msg = JSON.parse(rawData);\n\n if (msg.type === 'trigger' || msg.type === 'continue' || msg.type === 'stop') {\n await session.handleSocketMessage(msg as SocketMessage, {\n onEvent: (event) => conn.write(JSON.stringify(event)),\n onFinish: async () => {\n // Fetch and persist messages to your database for restoration\n },\n });\n }\n});\n```\n\nThe `handleSocketMessage()` method:\n\n- Handles `trigger`, `continue`, and `stop` messages\n- Automatically aborts previous requests when a new one arrives\n- Streams events via the `onEvent` callback\n- Calls `onFinish` after streaming completes (not called if aborted)\n\nSee [Socket Chat Example](/docs/examples/socket-chat) for a complete implementation.\n\n## Session Lifecycle\n\n```mermaid\nflowchart TD\n A[1. CREATE] --> B[2. ATTACH]\n B --> C[3. TRIGGER]\n C --> C\n C --> D[4. RETRIEVE]\n D --> C\n C --> E[5. EXPIRE]\n C --> G[5b. CLEAR]\n G --> F\n E --> F{6. RESTORE?}\n F -->|Yes| C\n F -->|No| A\n\n A -.- A1[\"`**client.agentSessions.create()**\n Returns sessionId\n Initializes state`\"]\n\n B -.- B1[\"`**client.agentSessions.attach()**\n Configure tool handlers\n Configure resource watchers`\"]\n\n C -.- C1[\"`**session.execute()**\n Execute request\n Stream events\n Update state`\"]\n\n D -.- D1[\"`**client.agentSessions.getMessages()**\n Get UI-ready messages\n Check session status`\"]\n\n E -.- E1[\"`Sessions expire after\n 24 hours (configurable)`\"]\n\n G -.- G1[\"`**client.agentSessions.clear()**\n Programmatically clear state\n Session becomes expired`\"]\n\n F -.- F1[\"`**client.agentSessions.restore()**\n Restore from stored messages\n Or create new session`\"]\n```\n\n## Session Expiration\n\nSessions expire after a period of inactivity (default: 24 hours). When you call `getMessages()` or `get()`, the response includes a `status` field:\n\n```typescript\nconst result = await client.agentSessions.getMessages(sessionId);\n\nif (result.status === 'expired') {\n // Session has expired - restore or create new\n console.log('Session expired:', result.sessionId);\n} else {\n // Session is active\n console.log('Messages:', result.messages.length);\n}\n```\n\n### Response Types\n\n| Status | Type | Description |\n| --------- | --------------------- | ------------------------------------------------------------- |\n| `active` | `UISessionState` | Session is active, includes `messages` array |\n| `expired` | `ExpiredSessionState` | Session expired, includes `sessionId`, `agentId`, `createdAt` |\n\n## Persisting Chat History\n\nTo enable session restoration, store the chat messages in your own database after each interaction:\n\n```typescript\n// After each trigger completes, save messages\nconst result = await client.agentSessions.getMessages(sessionId);\n\nif (result.status === 'active') {\n // Store in your database\n await db.chats.update({\n where: { id: chatId },\n data: {\n sessionId: result.sessionId,\n messages: result.messages, // Store UIMessage[] as JSON\n },\n });\n}\n```\n\n> **Best Practice**: Store the full `UIMessage[]` array. This preserves all message parts (text, tool calls, files, etc.) needed for accurate restoration.\n\n## Restoring Sessions\n\nWhen a user returns to your app:\n\n```typescript\n// 1. Load stored data from your database\nconst chat = await db.chats.findUnique({ where: { id: chatId } });\n\n// 2. Check if session is still active\nconst result = await client.agentSessions.getMessages(chat.sessionId);\n\nif (result.status === 'active') {\n // Session is active - use it directly\n return {\n sessionId: result.sessionId,\n messages: result.messages,\n };\n}\n\n// 3. Session expired - restore from stored messages\nif (chat.messages && chat.messages.length > 0) {\n const restored = await client.agentSessions.restore(\n chat.sessionId,\n chat.messages,\n { COMPANY_NAME: 'Acme Corp' }, // Optional: same input as create()\n );\n\n if (restored.restored) {\n // Session restored successfully\n return {\n sessionId: restored.sessionId,\n messages: chat.messages,\n };\n }\n}\n\n// 4. Cannot restore - create new session\nconst newSessionId = await client.agentSessions.create('support-chat', {\n COMPANY_NAME: 'Acme Corp',\n});\n\nreturn {\n sessionId: newSessionId,\n messages: [],\n};\n```\n\n### Restore Response\n\n```typescript\ninterface RestoreSessionResult {\n sessionId: string;\n restored: boolean; // true if restored, false if session was already active\n}\n```\n\n## Complete Example\n\nHere's a complete session management flow:\n\n```typescript\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n});\n\nasync function getOrCreateSession(chatId: string, agentId: string, input: Record<string, unknown>) {\n // Load existing chat data\n const chat = await db.chats.findUnique({ where: { id: chatId } });\n\n if (chat?.sessionId) {\n // Check session status\n const result = await client.agentSessions.getMessages(chat.sessionId);\n\n if (result.status === 'active') {\n return { sessionId: result.sessionId, messages: result.messages };\n }\n\n // Try to restore expired session\n if (chat.messages?.length > 0) {\n const restored = await client.agentSessions.restore(chat.sessionId, chat.messages, input);\n if (restored.restored) {\n return { sessionId: restored.sessionId, messages: chat.messages };\n }\n }\n }\n\n // Create new session\n const sessionId = await client.agentSessions.create(agentId, input);\n\n // Save to database\n await db.chats.upsert({\n where: { id: chatId },\n create: { id: chatId, sessionId, messages: [] },\n update: { sessionId, messages: [] },\n });\n\n return { sessionId, messages: [] };\n}\n```\n\n## Clearing Sessions\n\nTo programmatically clear a session's state (e.g., for testing reset/restore flows), use `clear()`:\n\n```typescript\nconst result = await client.agentSessions.clear(sessionId);\nconsole.log(result.cleared); // true\n```\n\nAfter clearing, the session transitions to `expired` status. You can then restore it with `restore()` or create a new session.\n\n```typescript\ninterface ClearSessionResult {\n sessionId: string;\n cleared: boolean;\n}\n```\n\nThis is idempotent - calling `clear()` on an already expired session succeeds without error.\n\n## Error Handling\n\n```typescript\nimport { ApiError } from '@octavus/server-sdk';\n\ntry {\n const session = await client.agentSessions.getMessages(sessionId);\n} catch (error) {\n if (error instanceof ApiError) {\n if (error.status === 404) {\n // Session not found or expired\n console.log('Session expired, create a new one');\n } else {\n console.error('API Error:', error.message);\n }\n }\n throw error;\n}\n```\n",
|
|
36
36
|
excerpt: "Sessions Sessions represent conversations with an agent. They store conversation history, track resources and variables, and enable stateful interactions. Creating Sessions Create a session by...",
|
|
37
37
|
order: 2
|
|
38
38
|
},
|
|
@@ -41,8 +41,8 @@ var docs_default = [
|
|
|
41
41
|
section: "server-sdk",
|
|
42
42
|
title: "Tools",
|
|
43
43
|
description: "Implementing tool handlers with the Server SDK.",
|
|
44
|
-
content: "\n# Tools\n\nTools extend what agents can do. In Octavus, tools can execute either on your server or on the client side.\n\n## Server Tools vs Client Tools\n\n| Location
|
|
45
|
-
excerpt: "Tools Tools extend what agents can do. In Octavus, tools can execute either on your server or on the client side. Server Tools vs Client Tools | Location
|
|
44
|
+
content: "\n# Tools\n\nTools extend what agents can do. In Octavus, tools can execute either on your server or on the client side.\n\n## Server Tools vs Client Tools\n\n| Location | Use Case | Registration |\n| -------------- | ------------------------------------------------- | -------------------------------------------------------- |\n| **Server** | Database queries, API calls, sensitive operations | Register handler in `attach()` |\n| **Inline MCP** | Group an integration's tools (GitHub, Salesforce) | [`createInlineMcpServer()`](/docs/server-sdk/inline-mcp) |\n| **Computer** | Browser, filesystem, shell, external services | [`session.setDynamicTools()`](/docs/server-sdk/computer) |\n| **Client** | Browser APIs, interactive UIs, confirmations | No server handler (forwarded to client) |\n\nWhen the Server SDK encounters a tool call:\n\n1. **Handler exists** (server, inline MCP, or dynamic) \u2192 Execute on server, continue automatically\n2. **No handler** \u2192 Forward to client via `client-tool-request` event\n\nInline MCP tools and dynamic tools registered via `session.setDynamicTools()` (e.g., from `@octavus/computer`) work identically to manual handlers from the platform's perspective. See [Inline MCP Servers](/docs/server-sdk/inline-mcp) for namespaced consumer-defined tool groups, and [Computer](/docs/server-sdk/computer) for device-side MCPs.\n\nFor client-side tool handling, see [Client Tools](/docs/client-sdk/client-tools).\n\n## Why Server Tools\n\nServer-side tools give you full control:\n\n- \u2705 **Full data access** - Query your database directly\n- \u2705 **Your authentication** - Use your existing auth context\n- \u2705 **No data exposure** - Sensitive data never leaves your infrastructure\n- \u2705 **Custom logic** - Any complexity you need\n\n## Defining Tool Handlers\n\nTool handlers are async functions that receive arguments and return results:\n\n```typescript\nimport type { ToolHandlers } from '@octavus/server-sdk';\n\nconst tools: ToolHandlers = {\n 'get-user-account': async (args) => {\n const userId = args.userId as string;\n\n // Query your database\n const user = await db.users.findById(userId);\n\n return {\n name: user.name,\n email: user.email,\n plan: user.subscription.plan,\n createdAt: user.createdAt.toISOString(),\n };\n },\n\n 'create-support-ticket': async (args) => {\n const summary = args.summary as string;\n const priority = args.priority as string;\n\n // Create ticket in your system\n const ticket = await ticketService.create({\n summary,\n priority,\n source: 'ai-chat',\n });\n\n return {\n ticketId: ticket.id,\n estimatedResponse: getEstimatedResponse(priority),\n };\n },\n};\n```\n\n## Using Tools in Sessions\n\nPass tool handlers when attaching to a session:\n\n```typescript\nconst session = client.agentSessions.attach(sessionId, {\n tools: {\n 'get-user-account': async (args) => {\n // Implementation\n },\n 'create-support-ticket': async (args) => {\n // Implementation\n },\n },\n});\n```\n\n## Tool Handler Signature\n\n```typescript\ntype ToolHandler = (args: Record<string, unknown>) => Promise<unknown>;\ntype ToolHandlers = Record<string, ToolHandler>;\n```\n\n### Arguments\n\nArguments are passed as a `Record<string, unknown>`. Type-check as needed:\n\n```typescript\n'search-products': async (args) => {\n const query = args.query as string;\n const category = args.category as string | undefined;\n const maxPrice = args.maxPrice as number | undefined;\n\n return await productSearch({ query, category, maxPrice });\n}\n```\n\n### Return Values\n\nReturn any JSON-serializable value. The result is:\n\n1. Sent back to the LLM as context\n2. Stored in session state\n3. Optionally stored in a variable for protocol use\n\n```typescript\n// Return object\nreturn { id: '123', status: 'created' };\n\n// Return array\nreturn [{ id: '1' }, { id: '2' }];\n\n// Return primitive\nreturn 42;\n\n// Return null for \"no result\"\nreturn null;\n```\n\n## Error Handling\n\nThrow errors for failures. They're captured and sent to the LLM:\n\n```typescript\n'get-user-account': async (args) => {\n const userId = args.userId as string;\n\n const user = await db.users.findById(userId);\n\n if (!user) {\n throw new Error(`User not found: ${userId}`);\n }\n\n return user;\n}\n```\n\nThe LLM receives the error message and can respond appropriately (e.g., \"I couldn't find that account\").\n\n## Tool Execution Flow\n\nWhen the LLM calls a tool:\n\n```mermaid\nsequenceDiagram\n participant LLM\n participant Platform as Octavus Platform\n participant SDK as Server SDK\n participant Client as Client SDK\n\n LLM->>Platform: 1. Decides to call tool\n Platform-->>Client: tool-input-start, tool-input-delta\n Platform-->>Client: tool-input-available\n Platform-->>SDK: 2. tool-request (stream pauses)\n\n alt Has server handler\n Note over SDK: 3a. Execute handler<br/>tools['get-user']()\n SDK-->>Client: tool-output-available\n SDK->>Platform: Continue with results\n else No server handler\n SDK-->>Client: 3b. client-tool-request\n Note over Client: Execute client tool<br/>or show interactive UI\n Client->>SDK: Tool results\n SDK->>Platform: Continue with results\n end\n\n Platform->>LLM: 4. Process results\n LLM-->>Platform: Response\n Platform-->>Client: text-delta events\n\n Note over LLM,Client: 5. Repeat if more tools needed\n```\n\n## Accessing Request Context\n\nFor request-specific data (auth, headers), create handlers dynamically:\n\n```typescript\nimport { toSSEStream } from '@octavus/server-sdk';\n\n// In your API route\nexport async function POST(request: Request) {\n const body = await request.json();\n const { sessionId, ...payload } = body;\n\n const authToken = request.headers.get('Authorization');\n const user = await validateToken(authToken);\n\n const session = client.agentSessions.attach(sessionId, {\n tools: {\n 'get-user-account': async (args) => {\n // Use request context\n return await db.users.findById(user.id);\n },\n 'create-order': async (args) => {\n // Create with user context\n return await orderService.create({\n ...args,\n userId: user.id,\n createdBy: user.email,\n });\n },\n // Tools without handlers here are forwarded to the client\n },\n });\n\n const events = session.execute(payload, { signal: request.signal });\n return new Response(toSSEStream(events));\n}\n```\n\n## Best Practices\n\n### 1. Validate Arguments\n\n```typescript\n'create-ticket': async (args) => {\n const summary = args.summary;\n if (typeof summary !== 'string' || summary.length === 0) {\n throw new Error('Summary is required');\n }\n // ...\n}\n```\n\n### 2. Handle Timeouts\n\n```typescript\n'external-api-call': async (args) => {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 10000);\n\n try {\n const response = await fetch(url, { signal: controller.signal });\n return await response.json();\n } finally {\n clearTimeout(timeout);\n }\n}\n```\n\n### 3. Log Tool Calls\n\n```typescript\n'search-products': async (args) => {\n console.log('Tool call: search-products', { args });\n\n const result = await productSearch(args);\n\n console.log('Tool result: search-products', {\n resultCount: result.length\n });\n\n return result;\n}\n```\n",
|
|
45
|
+
excerpt: "Tools Tools extend what agents can do. In Octavus, tools can execute either on your server or on the client side. Server Tools vs Client Tools | Location | Use Case ...",
|
|
46
46
|
order: 3
|
|
47
47
|
},
|
|
48
48
|
{
|
|
@@ -59,7 +59,7 @@ var docs_default = [
|
|
|
59
59
|
section: "server-sdk",
|
|
60
60
|
title: "CLI",
|
|
61
61
|
description: "Command-line interface for validating and syncing agent definitions.",
|
|
62
|
-
content: '\n# Octavus CLI\n\nThe `@octavus/cli` package provides a command-line interface for validating and syncing agent definitions from your local filesystem to the Octavus platform.\n\n**Current version:** `3.
|
|
62
|
+
content: '\n# Octavus CLI\n\nThe `@octavus/cli` package provides a command-line interface for validating and syncing agent definitions from your local filesystem to the Octavus platform.\n\n**Current version:** `3.5.0`\n\n## Installation\n\n```bash\nnpm install --save-dev @octavus/cli\n```\n\n## Configuration\n\nThe CLI requires an API key with the **Agents** permission.\n\n### Environment Variables\n\n| Variable | Description |\n| --------------------- | ---------------------------------------------- |\n| `OCTAVUS_CLI_API_KEY` | API key with "Agents" permission (recommended) |\n| `OCTAVUS_API_KEY` | Fallback if `OCTAVUS_CLI_API_KEY` not set |\n| `OCTAVUS_API_URL` | Optional, defaults to `https://octavus.ai` |\n\n### Two-Key Strategy (Recommended)\n\nFor production deployments, use separate API keys with minimal permissions:\n\n```bash\n# CI/CD or .env.local (not committed)\nOCTAVUS_CLI_API_KEY=oct_sk_... # "Agents" permission only\n\n# Production .env\nOCTAVUS_API_KEY=oct_sk_... # "Sessions" permission only\n```\n\nThis ensures production servers only have session permissions (smaller blast radius if leaked), while agent management is restricted to development/CI environments.\n\n### Multiple Environments\n\nUse separate Octavus projects for staging and production, each with their own API keys. The `--env` flag lets you load different environment files:\n\n```bash\n# Local development (default: .env)\noctavus sync ./agents/my-agent\n\n# Staging project\noctavus --env .env.staging sync ./agents/my-agent\n\n# Production project\noctavus --env .env.production sync ./agents/my-agent\n```\n\nExample environment files:\n\n```bash\n# .env.staging (syncs to your staging project)\nOCTAVUS_CLI_API_KEY=oct_sk_staging_project_key...\n\n# .env.production (syncs to your production project)\nOCTAVUS_CLI_API_KEY=oct_sk_production_project_key...\n```\n\nEach project has its own agents, so you\'ll get different agent IDs per environment.\n\n## Global Options\n\n| Option | Description |\n| -------------- | ------------------------------------------------------- |\n| `--env <file>` | Load environment from a specific file (default: `.env`) |\n| `--help` | Show help |\n| `--version` | Show version |\n\n## Commands\n\n### `octavus sync <path>`\n\nSync an agent definition to the platform. Creates the agent if it doesn\'t exist, or updates it if it does.\n\n```bash\noctavus sync ./agents/my-agent\n```\n\n**Options:**\n\n- `--json` - Output as JSON (for CI/CD parsing)\n- `--quiet` - Suppress non-essential output\n\n**Example output:**\n\n```\n\u2139 Reading agent from ./agents/my-agent...\n\u2139 Syncing support-chat...\n\u2713 Created: support-chat\n Agent ID: clxyz123abc456\n```\n\n### `octavus validate <path>`\n\nValidate an agent definition without saving. Useful for CI/CD pipelines.\n\n```bash\noctavus validate ./agents/my-agent\n```\n\n**Exit codes:**\n\n- `0` - Validation passed\n- `1` - Validation errors\n- `2` - Configuration errors (missing API key, etc.)\n\n### `octavus list`\n\nList all agents in your project.\n\n```bash\noctavus list\n```\n\n**Example output:**\n\n```\nSLUG NAME FORMAT ID\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nsupport-chat Support Chat Agent interactive clxyz123abc456\n\n1 agent(s)\n```\n\n### `octavus get <slug>`\n\nGet details about a specific agent by its slug.\n\n```bash\noctavus get support-chat\n```\n\n### `octavus archive <slug>`\n\nArchive an agent by slug (soft delete). Archived agents are removed from the active agent list and their slug is freed for reuse.\n\n```bash\noctavus archive support-chat\n```\n\n**Options:**\n\n- `--json` - Output as JSON (for CI/CD parsing)\n- `--quiet` - Suppress non-essential output\n\n**Example output:**\n\n```\n\u2139 Archiving support-chat...\n\u2713 Archived: support-chat\n Agent ID: clxyz123abc456\n```\n\n### `octavus skills sync <path>`\n\nSync a skill to the platform. Packages the skill directory into a bundle (excluding `.env` files, `.git`, and `node_modules`), uploads it, and optionally pushes secrets from the skill\'s `.env` file.\n\n```bash\noctavus skills sync ./skills/github\n```\n\n**Options:**\n\n- `--json` - Output as JSON (for CI/CD parsing)\n- `--quiet` - Suppress non-essential output\n\n**Example output:**\n\n```\n\u2139 Reading skill from ./skills/github...\n\u2139 Packaging github...\n\u2713 Created: github\n Skill ID: clxyz789def012\n\u2139 Pushing 2 secret(s)...\n\u2713 2 secret(s) updated\n```\n\n**Secret handling:**\n\nIf the skill directory contains a `.env` file, secrets are pushed alongside the bundle. Secrets are cross-validated against the `secrets` declarations in `SKILL.md` - warnings are shown for undeclared or missing required secrets.\n\n```\nmy-skill/\n\u251C\u2500\u2500 SKILL.md\n\u251C\u2500\u2500 scripts/\n\u2502 \u2514\u2500\u2500 run.py\n\u2514\u2500\u2500 .env # Secrets (not included in bundle)\n```\n\nSee [Skills](/docs/protocol/skills) for details on skill format, secrets, and secure mode.\n\n## Agent Directory Structure\n\nThe CLI expects agent definitions in a specific directory structure:\n\n```\nmy-agent/\n\u251C\u2500\u2500 settings.json # Required: Agent metadata\n\u251C\u2500\u2500 protocol.yaml # Required: Agent protocol\n\u251C\u2500\u2500 prompts/ # Optional: Prompt templates\n\u2502 \u251C\u2500\u2500 system.md\n\u2502 \u2514\u2500\u2500 user-message.md\n\u2514\u2500\u2500 references/ # Optional: Reference documents\n \u2514\u2500\u2500 api-guidelines.md\n```\n\n### references/\n\nReference files are markdown documents with YAML frontmatter containing a `description`. The agent can fetch these on demand during execution. See [References](/docs/protocol/references) for details.\n\n### settings.json\n\n```json\n{\n "slug": "my-agent",\n "name": "My Agent",\n "description": "A helpful assistant",\n "format": "interactive"\n}\n```\n\n### protocol.yaml\n\nSee the [Protocol documentation](/docs/protocol/overview) for details on protocol syntax.\n\n## CI/CD Integration\n\n### GitHub Actions\n\n```yaml\nname: Validate and Sync Agents\n\non:\n push:\n branches: [main]\n paths:\n - \'agents/**\'\n\njobs:\n sync:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-node@v4\n with:\n node-version: \'22\'\n\n - run: npm install\n\n - name: Validate agent\n run: npx octavus validate ./agents/support-chat\n env:\n OCTAVUS_CLI_API_KEY: ${{ secrets.OCTAVUS_CLI_API_KEY }}\n\n - name: Sync agent\n run: npx octavus sync ./agents/support-chat\n env:\n OCTAVUS_CLI_API_KEY: ${{ secrets.OCTAVUS_CLI_API_KEY }}\n```\n\n### Package.json Scripts\n\nAdd sync scripts to your `package.json`:\n\n```json\n{\n "scripts": {\n "agents:validate": "octavus validate ./agents/my-agent",\n "agents:sync": "octavus sync ./agents/my-agent"\n },\n "devDependencies": {\n "@octavus/cli": "^0.1.0"\n }\n}\n```\n\n## Workflow\n\nThe recommended workflow for managing agents:\n\n1. **Define agent locally** - Create `settings.json`, `protocol.yaml`, and prompts\n2. **Validate** - Run `octavus validate ./my-agent` to check for errors\n3. **Sync** - Run `octavus sync ./my-agent` to push to platform\n4. **Store agent ID** - Save the output ID in an environment variable\n5. **Use in app** - Read the ID from env and pass to `client.agentSessions.create()`\n\n```bash\n# After syncing: octavus sync ./agents/support-chat\n# Output: Agent ID: clxyz123abc456\n\n# Add to your .env file\nOCTAVUS_SUPPORT_AGENT_ID=clxyz123abc456\n```\n\n```typescript\nconst agentId = process.env.OCTAVUS_SUPPORT_AGENT_ID;\n\nconst sessionId = await client.agentSessions.create(agentId, {\n COMPANY_NAME: \'Acme Corp\',\n});\n```\n',
|
|
63
63
|
excerpt: "Octavus CLI The package provides a command-line interface for validating and syncing agent definitions from your local filesystem to the Octavus platform. Current version: Installation ...",
|
|
64
64
|
order: 5
|
|
65
65
|
},
|
|
@@ -86,16 +86,25 @@ var docs_default = [
|
|
|
86
86
|
section: "server-sdk",
|
|
87
87
|
title: "Computer",
|
|
88
88
|
description: "Adding browser, filesystem, and shell capabilities to agents with @octavus/computer.",
|
|
89
|
-
content: "\n# Computer\n\nThe `@octavus/computer` package gives agents access to a physical or virtual machine's browser, filesystem, and shell. It connects to [MCP](https://modelcontextprotocol.io) servers, discovers their tools, and provides them to the server-sdk.\n\n**Current version:** `3.3.0`\n\n## Installation\n\n```bash\nnpm install @octavus/computer\n```\n\n## Quick Start\n\n```typescript\nimport { Computer } from '@octavus/computer';\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', ['--browser-url=http://127.0.0.1:9222']),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', ['/path/to/workspace']),\n shell: Computer.shell({ cwd: '/path/to/workspace', mode: 'unrestricted' }),\n },\n});\n\nawait computer.start();\n\nconst client = new OctavusClient({\n baseUrl: 'https://octavus.ai',\n apiKey: 'your-api-key',\n});\n\nconst session = client.agentSessions.attach(sessionId, {\n tools: {\n 'set-chat-title': async (args) => ({ title: args.title }),\n },\n});\n\nsession.setDynamicTools(computer);\n```\n\nDynamic tools are registered after attaching via `session.setDynamicTools()`. Pass the `computer` directly - the session extracts schemas and handlers from the `ToolProvider`. Tool schemas are sent to the platform on the next `execute()` call, and tool calls flow back through the existing execution loop.\n\n## How It Works\n\n1. You configure MCP servers with namespaces (e.g., `browser`, `filesystem`, `shell`)\n2. `computer.start()` connects to all servers in parallel and discovers their tools\n3. Each tool is namespaced with `__` (e.g., `browser__navigate_page`, `filesystem__read_file`)\n4. The server-sdk sends tool schemas to the platform and handles tool call execution\n\nThe agent's protocol must declare matching `mcpServers` with `source: device` - see [MCP Servers](/docs/protocol/mcp-servers).\n\n## Entry Types\n\nThe `Computer` class supports three types of MCP entries:\n\n### Stdio (MCP Subprocess)\n\nSpawns an MCP server as a child process, communicating via stdin/stdout:\n\n```typescript\nComputer.stdio(command: string, args?: string[], options?: {\n env?: Record<string, string>;\n cwd?: string;\n})\n```\n\nUse this for local MCP servers installed as npm packages or standalone executables:\n\n```typescript\nconst computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', [\n '--browser-url=http://127.0.0.1:9222',\n '--no-usage-statistics',\n ]),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', [\n '/Users/me/projects/my-app',\n ]),\n },\n});\n```\n\n### HTTP (Remote MCP Endpoint)\n\nConnects to an MCP server over Streamable HTTP:\n\n```typescript\nComputer.http(url: string, options?: {\n headers?: Record<string, string>;\n})\n```\n\nUse this for MCP servers running as HTTP services:\n\n```typescript\nconst computer = new Computer({\n mcpServers: {\n docs: Computer.http('http://localhost:3001/mcp', {\n headers: { Authorization: 'Bearer token' },\n }),\n },\n});\n```\n\n### Shell (Built-in)\n\nProvides shell command execution without spawning an MCP subprocess:\n\n```typescript\nComputer.shell(options: {\n cwd?: string;\n mode: ShellMode;\n timeout?: number; // Default: 300,000ms (5 minutes)\n})\n```\n\nThis exposes a `run_command` tool (namespaced as `shell__run_command` when the key is `shell`). Commands execute in a login shell with the user's full environment.\n\n```typescript\nconst computer = new Computer({\n mcpServers: {\n shell: Computer.shell({\n cwd: '/Users/me/projects/my-app',\n mode: 'unrestricted',\n timeout: 300_000,\n }),\n },\n});\n```\n\n#### Shell Safety Modes\n\n| Mode | Description |\n| -------------------------------------- | --------------------------------------------- |\n| `'unrestricted'` | All commands allowed (for dedicated machines) |\n| `{ allowedPatterns, blockedPatterns }` | Pattern-based command filtering |\n\nPattern-based filtering:\n\n```typescript\nComputer.shell({\n cwd: workspaceDir,\n mode: {\n blockedPatterns: [/rm\\s+-rf/, /sudo/],\n allowedPatterns: [/^git\\s/, /^npm\\s/, /^ls\\s/],\n },\n});\n```\n\nWhen `allowedPatterns` is set, only matching commands are permitted. When `blockedPatterns` is set, matching commands are rejected. Blocked patterns are checked first.\n\n## Lifecycle\n\n### Starting\n\n`computer.start()` connects to all configured MCP servers in parallel. If some servers fail to connect, the computer still starts with the remaining servers - only if _all_ connections fail does it throw an error.\n\n```typescript\nconst { errors } = await computer.start();\n\nif (errors.length > 0) {\n console.warn('Some MCP servers failed to connect:', errors);\n}\n```\n\n### Stopping\n\n`computer.stop()` closes all MCP connections and kills managed processes:\n\n```typescript\nawait computer.stop();\n```\n\nAlways call `stop()` when the session ends to clean up MCP subprocesses. For managed processes (like Chrome), pass them in the config for automatic cleanup.\n\n## Dynamic Entries\n\nYou can add or remove MCP entries on a running `Computer` after `start()` has returned. This is useful when MCP configurations arrive after construction - for example, when a session-manager receives per-session entries from a dispatch payload and wants to wire them into the existing computer instead of rebuilding it.\n\n### `addEntry(namespace, entry, options?)`\n\nRegisters a new MCP entry under `namespace`. By default, connects immediately:\n\n```typescript\nawait computer.addEntry(\n 'github',\n Computer.stdio('@modelcontextprotocol/server-github', [], {\n env: { GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GH_TOKEN! },\n }),\n);\n```\n\nPass `{ deferred: true }` to register the entry without connecting. The entry starts in a degraded state and connects on the next `restartEntry(namespace)` call - useful for lazy MCPs the agent activates on demand:\n\n```typescript\nawait computer.addEntry('github', githubEntry, { deferred: true });\n\n// Later, when the agent decides it needs GitHub:\nawait computer.restartEntry('github');\n```\n\n`addEntry` throws if the namespace already exists. To replace an entry, call `removeEntry` first.\n\nIf the immediate connection fails, `addEntry` does not throw - the entry is registered as degraded with the error message attached. Inspect via `getHealth()` or `restartEntry()` to retry.\n\n### `removeEntry(namespace)`\n\nCloses the entry's connection (if any) and drops it from the configuration. No-op when the namespace doesn't exist:\n\n```typescript\nawait computer.removeEntry('github');\n```\n\n### `restartEntry(namespace)`\n\nCloses the existing connection (if any) and reconnects with the current configuration:\n\n```typescript\nawait computer.restartEntry('github');\n```\n\nUse this to bring a deferred entry online for the first time, or to recover an entry that became degraded mid-session.\n\n### Detecting dynamic-entry support\n\nConsumers that work with arbitrary `ToolProvider` implementations can detect dynamic-entry capability with `isDynamicMcpProvider`:\n\n```typescript\nimport { isDynamicMcpProvider } from '@octavus/server-sdk';\n\nif (isDynamicMcpProvider(provider)) {\n await provider.addEntry('github', githubEntry);\n}\n```\n\n`Computer` always passes this check.\n\n## Chrome Launch Helper\n\nFor desktop applications that need to control a browser, `Computer.launchChrome()` launches Chrome with remote debugging enabled:\n\n```typescript\nconst browser = await Computer.launchChrome({\n profileDir: '/Users/me/.my-app/chrome-profiles/agent-1',\n debuggingPort: 9222, // Optional, auto-allocated if omitted\n flags: ['--window-size=1280,800'],\n});\n\nconsole.log(`Chrome running on port ${browser.port}, PID ${browser.pid}`);\n```\n\nPass the browser to `managedProcesses` for automatic cleanup when the computer stops:\n\n```typescript\nconst computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', [\n `--browser-url=http://127.0.0.1:${browser.port}`,\n ]),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', [workspaceDir]),\n shell: Computer.shell({ cwd: workspaceDir, mode: 'unrestricted' }),\n },\n managedProcesses: [{ process: browser.process }],\n});\n```\n\n### ChromeLaunchOptions\n\n| Field | Required | Description |\n| --------------- | -------- | ----------------------------------------------------- |\n| `profileDir` | Yes | Directory for Chrome's user data (profile isolation) |\n| `debuggingPort` | No | Port for remote debugging (auto-allocated if omitted) |\n| `flags` | No | Additional Chrome launch flags |\n\n## ToolProvider Interface\n\n`Computer` implements the `ToolProvider` interface from `@octavus/core`:\n\n```typescript\ninterface ToolProvider {\n toolHandlers(): Record<string, ToolHandler>;\n toolSchemas(): ToolSchema[];\n}\n```\n\n`setDynamicTools()` accepts any `ToolProvider` directly - the session extracts schemas and handlers automatically:\n\n```typescript\nsession.setDynamicTools(computer);\n```\n\nYou can also pass a custom `ToolProvider`:\n\n```typescript\nconst customProvider: ToolProvider = {\n toolHandlers() {\n return {\n custom__my_tool: async (args) => {\n return { result: 'done' };\n },\n };\n },\n toolSchemas() {\n return [\n {\n name: 'custom__my_tool',\n description: 'A custom tool',\n inputSchema: {\n type: 'object',\n properties: {\n input: { type: 'string', description: 'Tool input' },\n },\n required: ['input'],\n },\n },\n ];\n },\n};\n\nconst session = client.agentSessions.attach(sessionId, {\n tools: { 'set-chat-title': titleHandler },\n});\n\nsession.setDynamicTools(customProvider);\n```\n\nFor cases where you need explicit control, `setDynamicTools()` also accepts a `DynamicTool[]` array:\n\n```typescript\ninterface DynamicTool {\n schema: ToolSchema;\n handler: ToolHandler;\n}\n```\n\n## Complete Example\n\nA desktop application with browser, filesystem, and shell capabilities:\n\n```typescript\nimport { Computer } from '@octavus/computer';\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst WORKSPACE_DIR = '/Users/me/projects/my-app';\nconst PROFILE_DIR = '/Users/me/.my-app/chrome-profiles/agent';\n\nasync function startSession(sessionId: string) {\n // 1. Launch Chrome with remote debugging\n const browser = await Computer.launchChrome({\n profileDir: PROFILE_DIR,\n });\n\n // 2. Create computer with all capabilities\n const computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', [\n `--browser-url=http://127.0.0.1:${browser.port}`,\n '--no-usage-statistics',\n ]),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', [WORKSPACE_DIR]),\n shell: Computer.shell({\n cwd: WORKSPACE_DIR,\n mode: 'unrestricted',\n }),\n },\n managedProcesses: [{ process: browser.process }],\n });\n\n // 3. Connect to all MCP servers\n const { errors } = await computer.start();\n if (errors.length > 0) {\n console.warn('Failed to connect:', errors);\n }\n\n // 4. Attach to session and register dynamic tools\n const client = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n });\n\n const session = client.agentSessions.attach(sessionId, {\n tools: {\n 'set-chat-title': async (args) => {\n console.log('Chat title:', args.title);\n return { success: true };\n },\n },\n });\n\n session.setDynamicTools(computer);\n\n // 5. Execute and stream\n const events = session.execute({\n type: 'trigger',\n triggerName: 'user-message',\n input: { USER_MESSAGE: 'Navigate to github.com and take a screenshot' },\n });\n\n for await (const event of events) {\n // Handle stream events\n }\n\n // 6. Clean up\n await computer.stop();\n}\n```\n\n## API Reference\n\n### Computer\n\n```typescript\nclass Computer implements ToolProvider {\n constructor(config: ComputerConfig);\n\n // Static factories for MCP entries\n static stdio(\n command: string,\n args?: string[],\n options?: {\n env?: Record<string, string>;\n cwd?: string;\n },\n ): StdioConfig;\n\n static http(\n url: string,\n options?: {\n headers?: Record<string, string>;\n },\n ): HttpConfig;\n\n static shell(options: { cwd?: string; mode: ShellMode; timeout?: number }): ShellConfig;\n\n // Chrome launch helper\n static launchChrome(options: ChromeLaunchOptions): Promise<ChromeInstance>;\n\n // Lifecycle\n start(): Promise<{ errors: string[] }>;\n stop(): Promise<void>;\n\n // Dynamic entries\n addEntry(namespace: string, entry: McpEntry, options?: { deferred?: boolean }): Promise<void>;\n removeEntry(namespace: string): Promise<void>;\n restartEntry(namespace: string): Promise<void>;\n stopEntry(namespace: string): Promise<void>;\n\n // Health\n getHealth(): Promise<ComputerHealth>;\n ensureReady(): Promise<EnsureReadyResult>;\n retryDegraded(): Promise<{ recovered: string[]; stillDegraded: string[] }>;\n\n // ToolProvider implementation\n toolHandlers(): Record<string, ToolHandler>;\n toolSchemas(): ToolSchema[];\n}\n\ninterface ComputerHealth {\n healthy: boolean;\n entries: EntryHealth[];\n totalTools: number;\n}\n\ninterface EntryHealth {\n name: string;\n healthy: boolean;\n error?: string;\n}\n\ninterface EnsureReadyResult extends ComputerHealth {\n recovered?: string[];\n failedEntries?: string[];\n}\n```\n\n### ComputerConfig\n\n```typescript\ninterface ComputerConfig {\n mcpServers: Record<string, McpEntry>;\n managedProcesses?: { process: ChildProcess }[];\n /** Namespaces to skip during start() - they begin as degraded and can be connected on demand via restartEntry(). */\n deferredEntries?: string[];\n}\n\ntype McpEntry = StdioConfig | HttpConfig | ShellConfig;\ntype ShellMode =\n | 'unrestricted'\n | {\n allowedPatterns?: RegExp[];\n blockedPatterns?: RegExp[];\n };\n```\n\n### ChromeInstance\n\n```typescript\ninterface ChromeInstance {\n port: number;\n process: ChildProcess;\n pid: number;\n}\n```\n",
|
|
89
|
+
content: "\n# Computer\n\nThe `@octavus/computer` package gives agents access to a physical or virtual machine's browser, filesystem, and shell. It connects to [MCP](https://modelcontextprotocol.io) servers, discovers their tools, and provides them to the server-sdk.\n\n**Current version:** `3.5.0`\n\n## Installation\n\n```bash\nnpm install @octavus/computer\n```\n\n## Quick Start\n\n```typescript\nimport { Computer } from '@octavus/computer';\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', ['--browser-url=http://127.0.0.1:9222']),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', ['/path/to/workspace']),\n shell: Computer.shell({ cwd: '/path/to/workspace', mode: 'unrestricted' }),\n },\n});\n\nawait computer.start();\n\nconst client = new OctavusClient({\n baseUrl: 'https://octavus.ai',\n apiKey: 'your-api-key',\n});\n\nconst session = client.agentSessions.attach(sessionId, {\n tools: {\n 'set-chat-title': async (args) => ({ title: args.title }),\n },\n});\n\nsession.setDynamicTools(computer);\n```\n\nDynamic tools are registered after attaching via `session.setDynamicTools()`. Pass the `computer` directly - the session extracts schemas and handlers from the `ToolProvider`. Tool schemas are sent to the platform on the next `execute()` call, and tool calls flow back through the existing execution loop.\n\n## How It Works\n\n1. You configure MCP servers with namespaces (e.g., `browser`, `filesystem`, `shell`)\n2. `computer.start()` connects to all servers in parallel and discovers their tools\n3. Each tool is namespaced with `__` (e.g., `browser__navigate_page`, `filesystem__read_file`)\n4. The server-sdk sends tool schemas to the platform and handles tool call execution\n\nThe agent's protocol must declare matching `mcpServers` with `source: device` - see [MCP Servers](/docs/protocol/mcp-servers).\n\n## Entry Types\n\nThe `Computer` class supports three types of MCP entries:\n\n### Stdio (MCP Subprocess)\n\nSpawns an MCP server as a child process, communicating via stdin/stdout:\n\n```typescript\nComputer.stdio(command: string, args?: string[], options?: {\n env?: Record<string, string>;\n cwd?: string;\n})\n```\n\nUse this for local MCP servers installed as npm packages or standalone executables:\n\n```typescript\nconst computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', [\n '--browser-url=http://127.0.0.1:9222',\n '--no-usage-statistics',\n ]),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', [\n '/Users/me/projects/my-app',\n ]),\n },\n});\n```\n\n### HTTP (Remote MCP Endpoint)\n\nConnects to an MCP server over Streamable HTTP:\n\n```typescript\nComputer.http(url: string, options?: {\n headers?: Record<string, string>;\n})\n```\n\nUse this for MCP servers running as HTTP services:\n\n```typescript\nconst computer = new Computer({\n mcpServers: {\n docs: Computer.http('http://localhost:3001/mcp', {\n headers: { Authorization: 'Bearer token' },\n }),\n },\n});\n```\n\n### Shell (Built-in)\n\nProvides shell command execution without spawning an MCP subprocess:\n\n```typescript\nComputer.shell(options: {\n cwd?: string;\n mode: ShellMode;\n timeout?: number; // Default: 300,000ms (5 minutes)\n})\n```\n\nThis exposes a `run_command` tool (namespaced as `shell__run_command` when the key is `shell`). Commands execute in a login shell with the user's full environment.\n\n```typescript\nconst computer = new Computer({\n mcpServers: {\n shell: Computer.shell({\n cwd: '/Users/me/projects/my-app',\n mode: 'unrestricted',\n timeout: 300_000,\n }),\n },\n});\n```\n\n#### Shell Safety Modes\n\n| Mode | Description |\n| -------------------------------------- | --------------------------------------------- |\n| `'unrestricted'` | All commands allowed (for dedicated machines) |\n| `{ allowedPatterns, blockedPatterns }` | Pattern-based command filtering |\n\nPattern-based filtering:\n\n```typescript\nComputer.shell({\n cwd: workspaceDir,\n mode: {\n blockedPatterns: [/rm\\s+-rf/, /sudo/],\n allowedPatterns: [/^git\\s/, /^npm\\s/, /^ls\\s/],\n },\n});\n```\n\nWhen `allowedPatterns` is set, only matching commands are permitted. When `blockedPatterns` is set, matching commands are rejected. Blocked patterns are checked first.\n\n## Lifecycle\n\n### Starting\n\n`computer.start()` connects to all configured MCP servers in parallel. If some servers fail to connect, the computer still starts with the remaining servers - only if _all_ connections fail does it throw an error.\n\n```typescript\nconst { errors } = await computer.start();\n\nif (errors.length > 0) {\n console.warn('Some MCP servers failed to connect:', errors);\n}\n```\n\n### Stopping\n\n`computer.stop()` closes all MCP connections and kills managed processes:\n\n```typescript\nawait computer.stop();\n```\n\nAlways call `stop()` when the session ends to clean up MCP subprocesses. For managed processes (like Chrome), pass them in the config for automatic cleanup.\n\n## Dynamic Entries\n\nYou can add or remove MCP entries on a running `Computer` after `start()` has returned. This is useful when MCP configurations arrive after construction - for example, when a session-manager receives per-session entries from a dispatch payload and wants to wire them into the existing computer instead of rebuilding it.\n\n### `addEntry(namespace, entry, options?)`\n\nRegisters a new MCP entry under `namespace`. By default, connects immediately:\n\n```typescript\nawait computer.addEntry(\n 'github',\n Computer.stdio('@modelcontextprotocol/server-github', [], {\n env: { GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GH_TOKEN! },\n }),\n);\n```\n\nPass `{ deferred: true }` to register the entry without connecting. The entry starts in a degraded state and connects on the next `restartEntry(namespace)` call - useful for lazy MCPs the agent activates on demand:\n\n```typescript\nawait computer.addEntry('github', githubEntry, { deferred: true });\n\n// Later, when the agent decides it needs GitHub:\nawait computer.restartEntry('github');\n```\n\n`addEntry` throws if the namespace already exists. To replace an entry, call `removeEntry` first.\n\nIf the immediate connection fails, `addEntry` does not throw - the entry is registered as degraded with the error message attached. Inspect via `getHealth()` or `restartEntry()` to retry.\n\n### `removeEntry(namespace)`\n\nCloses the entry's connection (if any) and drops it from the configuration. No-op when the namespace doesn't exist:\n\n```typescript\nawait computer.removeEntry('github');\n```\n\n### `restartEntry(namespace)`\n\nCloses the existing connection (if any) and reconnects with the current configuration:\n\n```typescript\nawait computer.restartEntry('github');\n```\n\nUse this to bring a deferred entry online for the first time, or to recover an entry that became degraded mid-session.\n\n### Detecting dynamic-entry support\n\nConsumers that work with arbitrary `ToolProvider` implementations can detect dynamic-entry capability with `isDynamicMcpProvider`:\n\n```typescript\nimport { isDynamicMcpProvider } from '@octavus/server-sdk';\n\nif (isDynamicMcpProvider(provider)) {\n await provider.addEntry('github', githubEntry);\n}\n```\n\n`Computer` always passes this check.\n\n## Chrome Launch Helper\n\nFor desktop applications that need to control a browser, `Computer.launchChrome()` launches Chrome with remote debugging enabled:\n\n```typescript\nconst browser = await Computer.launchChrome({\n profileDir: '/Users/me/.my-app/chrome-profiles/agent-1',\n debuggingPort: 9222, // Optional, auto-allocated if omitted\n flags: ['--window-size=1280,800'],\n});\n\nconsole.log(`Chrome running on port ${browser.port}, PID ${browser.pid}`);\n```\n\nPass the browser to `managedProcesses` for automatic cleanup when the computer stops:\n\n```typescript\nconst computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', [\n `--browser-url=http://127.0.0.1:${browser.port}`,\n ]),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', [workspaceDir]),\n shell: Computer.shell({ cwd: workspaceDir, mode: 'unrestricted' }),\n },\n managedProcesses: [{ process: browser.process }],\n});\n```\n\n### ChromeLaunchOptions\n\n| Field | Required | Description |\n| --------------- | -------- | ----------------------------------------------------- |\n| `profileDir` | Yes | Directory for Chrome's user data (profile isolation) |\n| `debuggingPort` | No | Port for remote debugging (auto-allocated if omitted) |\n| `flags` | No | Additional Chrome launch flags |\n\n## ToolProvider Interface\n\n`Computer` implements the `ToolProvider` interface from `@octavus/core`:\n\n```typescript\ninterface ToolProvider {\n toolHandlers(): Record<string, ToolHandler>;\n toolSchemas(): ToolSchema[];\n}\n```\n\n`setDynamicTools()` accepts any `ToolProvider` directly - the session extracts schemas and handlers automatically:\n\n```typescript\nsession.setDynamicTools(computer);\n```\n\nYou can also pass a custom `ToolProvider`:\n\n```typescript\nconst customProvider: ToolProvider = {\n toolHandlers() {\n return {\n custom__my_tool: async (args) => {\n return { result: 'done' };\n },\n };\n },\n toolSchemas() {\n return [\n {\n name: 'custom__my_tool',\n description: 'A custom tool',\n inputSchema: {\n type: 'object',\n properties: {\n input: { type: 'string', description: 'Tool input' },\n },\n required: ['input'],\n },\n },\n ];\n },\n};\n\nconst session = client.agentSessions.attach(sessionId, {\n tools: { 'set-chat-title': titleHandler },\n});\n\nsession.setDynamicTools(customProvider);\n```\n\nFor cases where you need explicit control, `setDynamicTools()` also accepts a `DynamicTool[]` array:\n\n```typescript\ninterface DynamicTool {\n schema: ToolSchema;\n handler: ToolHandler;\n}\n```\n\n## Complete Example\n\nA desktop application with browser, filesystem, and shell capabilities:\n\n```typescript\nimport { Computer } from '@octavus/computer';\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst WORKSPACE_DIR = '/Users/me/projects/my-app';\nconst PROFILE_DIR = '/Users/me/.my-app/chrome-profiles/agent';\n\nasync function startSession(sessionId: string) {\n // 1. Launch Chrome with remote debugging\n const browser = await Computer.launchChrome({\n profileDir: PROFILE_DIR,\n });\n\n // 2. Create computer with all capabilities\n const computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', [\n `--browser-url=http://127.0.0.1:${browser.port}`,\n '--no-usage-statistics',\n ]),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', [WORKSPACE_DIR]),\n shell: Computer.shell({\n cwd: WORKSPACE_DIR,\n mode: 'unrestricted',\n }),\n },\n managedProcesses: [{ process: browser.process }],\n });\n\n // 3. Connect to all MCP servers\n const { errors } = await computer.start();\n if (errors.length > 0) {\n console.warn('Failed to connect:', errors);\n }\n\n // 4. Attach to session and register dynamic tools\n const client = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n });\n\n const session = client.agentSessions.attach(sessionId, {\n tools: {\n 'set-chat-title': async (args) => {\n console.log('Chat title:', args.title);\n return { success: true };\n },\n },\n });\n\n session.setDynamicTools(computer);\n\n // 5. Execute and stream\n const events = session.execute({\n type: 'trigger',\n triggerName: 'user-message',\n input: { USER_MESSAGE: 'Navigate to github.com and take a screenshot' },\n });\n\n for await (const event of events) {\n // Handle stream events\n }\n\n // 6. Clean up\n await computer.stop();\n}\n```\n\n## API Reference\n\n### Computer\n\n```typescript\nclass Computer implements ToolProvider {\n constructor(config: ComputerConfig);\n\n // Static factories for MCP entries\n static stdio(\n command: string,\n args?: string[],\n options?: {\n env?: Record<string, string>;\n cwd?: string;\n },\n ): StdioConfig;\n\n static http(\n url: string,\n options?: {\n headers?: Record<string, string>;\n },\n ): HttpConfig;\n\n static shell(options: { cwd?: string; mode: ShellMode; timeout?: number }): ShellConfig;\n\n // Chrome launch helper\n static launchChrome(options: ChromeLaunchOptions): Promise<ChromeInstance>;\n\n // Lifecycle\n start(): Promise<{ errors: string[] }>;\n stop(): Promise<void>;\n\n // Dynamic entries\n addEntry(namespace: string, entry: McpEntry, options?: { deferred?: boolean }): Promise<void>;\n removeEntry(namespace: string): Promise<void>;\n restartEntry(namespace: string): Promise<void>;\n stopEntry(namespace: string): Promise<void>;\n\n // Health\n getHealth(): Promise<ComputerHealth>;\n ensureReady(): Promise<EnsureReadyResult>;\n retryDegraded(): Promise<{ recovered: string[]; stillDegraded: string[] }>;\n\n // ToolProvider implementation\n toolHandlers(): Record<string, ToolHandler>;\n toolSchemas(): ToolSchema[];\n}\n\ninterface ComputerHealth {\n healthy: boolean;\n entries: EntryHealth[];\n totalTools: number;\n}\n\ninterface EntryHealth {\n name: string;\n healthy: boolean;\n error?: string;\n}\n\ninterface EnsureReadyResult extends ComputerHealth {\n recovered?: string[];\n failedEntries?: string[];\n}\n```\n\n### ComputerConfig\n\n```typescript\ninterface ComputerConfig {\n mcpServers: Record<string, McpEntry>;\n managedProcesses?: { process: ChildProcess }[];\n /** Namespaces to skip during start() - they begin as degraded and can be connected on demand via restartEntry(). */\n deferredEntries?: string[];\n}\n\ntype McpEntry = StdioConfig | HttpConfig | ShellConfig;\ntype ShellMode =\n | 'unrestricted'\n | {\n allowedPatterns?: RegExp[];\n blockedPatterns?: RegExp[];\n };\n```\n\n### ChromeInstance\n\n```typescript\ninterface ChromeInstance {\n port: number;\n process: ChildProcess;\n pid: number;\n}\n```\n",
|
|
90
90
|
excerpt: "Computer The package gives agents access to a physical or virtual machine's browser, filesystem, and shell. It connects to MCP servers, discovers their tools, and provides them to the server-sdk....",
|
|
91
91
|
order: 8
|
|
92
92
|
},
|
|
93
|
+
{
|
|
94
|
+
slug: "server-sdk/inline-mcp",
|
|
95
|
+
section: "server-sdk",
|
|
96
|
+
title: "Inline MCP Servers",
|
|
97
|
+
description: "Group an integration's tools into a Zod-typed bundle that runs in your server process.",
|
|
98
|
+
content: "\n# Inline MCP Servers\n\nInline MCP servers let you group an integration's tools (e.g., GitHub, Salesforce, an internal microservice) into a namespaced bundle with Zod-typed handler arguments. The tools execute in your server process via the same tool-request/continue path as ordinary [server tools](/docs/server-sdk/tools), so authentication and credentials stay in your process.\n\n## When to Use\n\nUse an inline MCP server when:\n\n- You're integrating a third-party API and want a logical grouping (`github__list-prs`, `github__get-issue`).\n- You want type-safe handler arguments instead of casting `args` from `unknown`.\n- You want to evolve the toolset without protocol-yaml round trips - tool names and schemas are sent at runtime.\n- Tool calls need credentials your platform should never see (OAuth tokens, customer API keys).\n\nFor comparison with the other tool registration paths, see the [tools overview](/docs/server-sdk/tools#server-tools-vs-client-tools).\n\n## Protocol Declaration\n\nDeclare the namespace in `protocol.yaml` with `source: consumer`. The platform learns the namespace and routes tool calls to your process; tool names and JSON schemas are provided by the SDK at runtime.\n\n```yaml\nmcpServers:\n github:\n description: Repository management - issues, pull requests, code\n source: consumer\n display: name\n\nagent:\n mcpServers:\n - github\n```\n\nSee [MCP Servers in the protocol reference](/docs/protocol/mcp-servers) for the full set of MCP source types and field semantics.\n\n## Defining the Server\n\n```typescript\nimport { z } from 'zod';\nimport { createInlineMcpServer, defineInlineMcpTool } from '@octavus/server-sdk';\n\nconst github = createInlineMcpServer('github', {\n tools: {\n 'get-pr-overview': defineInlineMcpTool({\n description: 'Get pull request metadata and file changes',\n parameters: z.object({\n owner: z.string(),\n repo: z.string(),\n pullNumber: z.number(),\n }),\n handler: async (args) => {\n // args is { owner: string; repo: string; pullNumber: number }\n return await githubService.getPrOverview(args.owner, args.repo, args.pullNumber);\n },\n }),\n\n 'list-issues': defineInlineMcpTool({\n description: 'List open issues for a repository',\n parameters: z.object({\n owner: z.string(),\n repo: z.string(),\n state: z.enum(['open', 'closed', 'all']).default('open'),\n }),\n handler: async (args) => {\n return await githubService.listIssues(args.owner, args.repo, args.state);\n },\n }),\n },\n});\n```\n\nThe factory:\n\n1. Validates the namespace and each tool name (see [naming rules](#naming-rules)).\n2. Converts each Zod schema to JSON Schema once at creation time.\n3. Returns an `InlineMcpServer` exposing `toolSchemas()` and `toolHandlers()`.\n\nThe resulting tool names are namespaced: `github__get-pr-overview`, `github__list-issues`.\n\n## Why `defineInlineMcpTool`\n\n`defineInlineMcpTool()` is a no-op at runtime, but it preserves Zod type inference. Without the wrapper, TypeScript collapses the per-tool generic when the tools are placed in a record literal, leaving `args` typed as `unknown`:\n\n```typescript\n// Without defineInlineMcpTool - args ends up as `unknown`\ntools: {\n 'get-pr-overview': {\n description: '...',\n parameters: z.object({ owner: z.string() }),\n handler: async (args) => args.owner, // \u274C TS error: args is 'unknown'\n },\n}\n\n// With defineInlineMcpTool - args inferred from the schema\ntools: {\n 'get-pr-overview': defineInlineMcpTool({\n description: '...',\n parameters: z.object({ owner: z.string() }),\n handler: async (args) => args.owner, // \u2713 args is { owner: string }\n }),\n}\n```\n\nThe handler also receives Zod-validated arguments. Invalid inputs throw before reaching your code, with the failed paths and messages joined into the error.\n\n## Attaching to a Session\n\nPass inline MCP servers via `mcpServers` on `attach()`. They merge with `tools` and survive across `setDynamicTools()` calls:\n\n```typescript\nconst session = client.agentSessions.attach(sessionId, {\n tools: {\n 'get-user-account': async (args) => db.users.findById(args.userId as string),\n },\n mcpServers: [github, salesforce],\n});\n```\n\nWorkers accept the same option:\n\n```typescript\nconst { output } = await client.workers.generate(agentId, input, {\n mcpServers: [github],\n});\n```\n\n## Authentication and Credentials\n\nHandlers close over your server's auth context, so credentials never leave your process. The platform receives the namespaced schema list and the tool call name; it never sees the keys you use to fulfill the call.\n\nA common pattern is to build the MCP server per-request when auth depends on the user:\n\n```typescript\nfunction buildGithubMcp(token: string) {\n const client = new GithubClient({ token });\n return createInlineMcpServer('github', {\n tools: {\n 'get-pr-overview': defineInlineMcpTool({\n description: 'Get pull request metadata and file changes',\n parameters: z.object({\n owner: z.string(),\n repo: z.string(),\n pullNumber: z.number(),\n }),\n handler: async (args) => client.pulls.get(args),\n }),\n },\n });\n}\n\nexport async function POST(request: Request) {\n const user = await authenticate(request);\n const session = client.agentSessions.attach(sessionId, {\n mcpServers: [buildGithubMcp(user.githubToken)],\n });\n\n const events = session.execute(payload, { signal: request.signal });\n return new Response(toSSEStream(events));\n}\n```\n\nFor static credentials (one tenant per deployment), build the server once at module scope.\n\n## How Tool Calls Flow\n\n1. The agent emits a `tool-request` event for `github__get-pr-overview` (or another inline-MCP-namespaced tool).\n2. The Server SDK looks up the handler registered by `createInlineMcpServer()` and runs it. Zod validates `args` against the tool's schema; a validation failure becomes a tool error sent back to the LLM.\n3. The handler returns; the SDK posts the result back to the platform via the same continuation request used for ordinary server tools.\n4. The platform feeds the result to the LLM and streams the next response chunk.\n\nThere is no separate transport - inline MCP tools ride on the same `dynamicToolSchemas` channel that device MCPs use, so no additional infrastructure is required.\n\n## Naming Rules\n\n`createInlineMcpServer()` validates the namespace and each tool name at construction time. Invalid values throw immediately:\n\n- **Namespace:** lowercase letters, digits, and hyphens; must start with a letter (`/^[a-z][a-z0-9-]*$/`).\n- **Tool name:** lowercase letters, digits, underscores, and hyphens; must start with a letter (`/^[a-z][a-z0-9_-]*$/`).\n\nThe resulting `${namespace}__${toolName}` is what the LLM sees and what flows through the platform's MCP routing.\n\n## Collision Rules\n\nThe resolver throws on the following conflicts so problems surface at attach time, not mid-stream:\n\n- Each inline MCP server's `namespace` must be unique across the array passed to `attach()` or the workers API.\n- A namespaced tool name (`namespace__tool`) cannot collide with a static tool handler key passed via `tools`.\n- A namespaced tool name cannot collide with a `dynamicToolSchemas` entry passed to the workers API.\n\nIf a tool registered via `setDynamicTools()` later collides with an inline MCP tool name, the dynamic handler wins for the duration of that dynamic-tool set; the inline MCP handler is restored on the next `setDynamicTools()` call that doesn't re-register the same name.\n\n## Inline vs Computer\n\nBoth inline MCP and the `Computer` integration register tools that flow through `dynamicToolSchemas`. Pick based on where the tool process runs:\n\n| Tool surface | Process location | Best for |\n| ------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------- |\n| Inline MCP | Your server (in-process closure) | Third-party APIs, internal microservices, anything where credentials live in your backend. |\n| Computer | The agent's machine (STDIO MCP) | Browser automation, filesystem, shell - device-local capabilities. See [Computer](/docs/server-sdk/computer). |\n\nThe two can coexist on the same session.\n",
|
|
99
|
+
excerpt: "Inline MCP Servers Inline MCP servers let you group an integration's tools (e.g., GitHub, Salesforce, an internal microservice) into a namespaced bundle with Zod-typed handler arguments. The tools...",
|
|
100
|
+
order: 9
|
|
101
|
+
},
|
|
93
102
|
{
|
|
94
103
|
slug: "client-sdk/overview",
|
|
95
104
|
section: "client-sdk",
|
|
96
105
|
title: "Overview",
|
|
97
106
|
description: "Introduction to the Octavus Client SDKs for building chat interfaces.",
|
|
98
|
-
content: "\n# Client SDK Overview\n\nOctavus provides two packages for frontend integration:\n\n| Package | Purpose | Use When |\n| --------------------- | ------------------------ | ----------------------------------------------------- |\n| `@octavus/react` | React hooks and bindings | Building React applications |\n| `@octavus/client-sdk` | Framework-agnostic core | Using Vue, Svelte, vanilla JS, or custom integrations |\n\n**Most users should install `@octavus/react`** - it includes everything from `@octavus/client-sdk` plus React-specific hooks.\n\n## Installation\n\n### React Applications\n\n```bash\nnpm install @octavus/react\n```\n\n**Current version:** `3.3.0`\n\n### Other Frameworks\n\n```bash\nnpm install @octavus/client-sdk\n```\n\n**Current version:** `3.3.0`\n\n## Transport Pattern\n\nThe Client SDK uses a **transport abstraction** to handle communication with your backend. This gives you flexibility in how events are delivered:\n\n| Transport | Use Case | Docs |\n| ----------------------- | -------------------------------------------- | ----------------------------------------------------- |\n| `createHttpTransport` | HTTP/SSE (Next.js, Express, etc.) | [HTTP Transport](/docs/client-sdk/http-transport) |\n| `createSocketTransport` | WebSocket, SockJS, or other socket protocols | [Socket Transport](/docs/client-sdk/socket-transport) |\n\nWhen the transport changes (e.g., when `sessionId` changes), the `useOctavusChat` hook automatically reinitializes with the new transport.\n\n> **Recommendation**: Use HTTP transport unless you specifically need WebSocket features (custom real-time events, Meteor/Phoenix, etc.).\n\n## React Usage\n\nThe `useOctavusChat` hook provides state management and streaming for React applications:\n\n```tsx\nimport { useMemo } from 'react';\nimport { useOctavusChat, createHttpTransport, type UIMessage } from '@octavus/react';\n\nfunction Chat({ sessionId }: { sessionId: string }) {\n // Create a stable transport instance (memoized on sessionId)\n const transport = useMemo(\n () =>\n createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n }),\n [sessionId],\n );\n\n const { messages, status, send } = useOctavusChat({ transport });\n\n const sendMessage = async (text: string) => {\n await send('user-message', { USER_MESSAGE: text }, { userMessage: { content: text } });\n };\n\n return (\n <div>\n {messages.map((msg) => (\n <MessageBubble key={msg.id} message={msg} />\n ))}\n </div>\n );\n}\n\nfunction MessageBubble({ message }: { message: UIMessage }) {\n return (\n <div>\n {message.parts.map((part, i) => {\n if (part.type === 'text') {\n return <p key={i}>{part.text}</p>;\n }\n return null;\n })}\n </div>\n );\n}\n```\n\n## Framework-Agnostic Usage\n\nThe `OctavusChat` class can be used with any framework or vanilla JavaScript:\n\n```typescript\nimport { OctavusChat, createHttpTransport } from '@octavus/client-sdk';\n\nconst transport = createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n});\n\nconst chat = new OctavusChat({ transport });\n\n// Subscribe to state changes\nconst unsubscribe = chat.subscribe(() => {\n console.log('Messages:', chat.messages);\n console.log('Status:', chat.status);\n // Update your UI here\n});\n\n// Send a message\nawait chat.send('user-message', { USER_MESSAGE: 'Hello' }, { userMessage: { content: 'Hello' } });\n\n// Cleanup when done\nunsubscribe();\n```\n\n## Key Features\n\n### Unified Send Function\n\nThe `send` function handles both user message display and agent triggering in one call:\n\n```tsx\nconst { send } = useOctavusChat({ transport });\n\n// Add user message to UI and trigger agent\nawait send('user-message', { USER_MESSAGE: text }, { userMessage: { content: text } });\n\n// Trigger without adding a user message (e.g., button click)\nawait send('request-human');\n```\n\n### Message Parts\n\nMessages contain ordered `parts` for rich content:\n\n```tsx\nconst { messages } = useOctavusChat({ transport });\n\n// Each message has typed parts\nmessage.parts.map((part) => {\n switch (part.type) {\n case 'text': // Text content\n case 'reasoning': // Extended reasoning/thinking\n case 'tool-call': // Tool execution\n case 'operation': // Internal operations (set-resource, etc.)\n }\n});\n```\n\n### Status Tracking\n\n```tsx\nconst { status } = useOctavusChat({ transport });\n\n// status: 'idle' | 'streaming' | 'error' | 'awaiting-input'\n// 'awaiting-input' occurs when interactive client tools need user action\n```\n\n### Stop Streaming\n\n```tsx\nconst { stop } = useOctavusChat({ transport });\n\n// Stop current stream and finalize message\nstop();\n```\n\n### Retry Last Trigger\n\nRe-execute the last trigger from the same starting point. Messages are rolled back to the state before the trigger, the user message is re-added (if any), and the agent re-executes. Already-uploaded files are reused without re-uploading.\n\n```tsx\nconst { retry, canRetry } = useOctavusChat({ transport });\n\n// Retry after an error, cancellation, or unsatisfactory result\nif (canRetry) {\n await retry();\n}\n```\n\n`canRetry` is `true` when a trigger has been sent and the chat is not currently streaming or awaiting input.\n\n## Hook Reference (React)\n\n### useOctavusChat\n\n```typescript\nfunction useOctavusChat(options: OctavusChatOptions): UseOctavusChatReturn;\n\ninterface OctavusChatOptions {\n // Required: Transport for streaming events\n transport: Transport;\n\n // Optional: Function to request upload URLs for file uploads\n requestUploadUrls?: (\n files: { filename: string; mediaType: string; size: number }[],\n ) => Promise<UploadUrlsResponse>;\n\n // Optional: Client-side tool handlers\n // - Function: executes automatically and returns result\n // - 'interactive': appears in pendingClientTools for user input\n clientTools?: Record<string, ClientToolHandler>;\n\n // Optional: Pre-populate with existing messages (session restore)\n initialMessages?: UIMessage[];\n\n // Optional: Callbacks\n onError?: (error: OctavusError) => void; // Structured error with type, source, retryable\n onFinish?: () => void;\n onStop?: () => void; // Called when user stops generation\n onResourceUpdate?: (name: string, value: unknown) => void;\n}\n\ninterface UseOctavusChatReturn {\n // State\n messages: UIMessage[];\n status: ChatStatus; // 'idle' | 'streaming' | 'error' | 'awaiting-input'\n error: OctavusError | null; // Structured error with type, source, retryable\n\n // Connection (socket transport only - undefined for HTTP)\n connectionState: ConnectionState | undefined; // 'disconnected' | 'connecting' | 'connected' | 'error'\n connectionError: Error | undefined;\n\n // Client tools (interactive tools awaiting user input)\n pendingClientTools: Record<string, InteractiveTool[]>; // Keyed by tool name\n\n // Actions\n send: (\n triggerName: string,\n input?: Record<string, unknown>,\n options?: { userMessage?: UserMessageInput },\n ) => Promise<void>;\n stop: () => void;\n retry: () => Promise<void>; // Retry last trigger from same starting point\n canRetry: boolean; // Whether retry() can be called\n\n // Connection management (socket transport only - undefined for HTTP)\n connect: (() => Promise<void>) | undefined;\n disconnect: (() => void) | undefined;\n\n // File uploads (requires requestUploadUrls)\n uploadFiles: (\n files: FileList | File[],\n onProgress?: (fileIndex: number, progress: number) => void,\n ) => Promise<FileReference[]>;\n}\n\ninterface UserMessageInput {\n content?: string;\n files?: FileList | File[] | FileReference[];\n}\n```\n\n### useAutoScroll\n\nSmart auto-scroll for chat containers. Scrolls to bottom when content updates, but pauses if the user has scrolled up. See [Streaming - Auto-Scroll](/docs/client-sdk/streaming#auto-scroll) for full usage.\n\n```typescript\nfunction useAutoScroll(options?: UseAutoScrollOptions): {\n scrollRef: RefObject<HTMLDivElement | null>;\n handleScroll: () => void;\n scrollOnUpdate: () => void;\n resetAutoScroll: () => void;\n};\n\ninterface UseAutoScrollOptions {\n scrollRef?: RefObject<HTMLDivElement | null>;\n threshold?: number; // Distance from bottom in px (default: 80)\n}\n```\n\n## Transport Reference\n\n### createHttpTransport\n\nCreates an HTTP/SSE transport using native `fetch()`:\n\n```typescript\nimport { createHttpTransport } from '@octavus/react';\n\nconst transport = createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n});\n```\n\n### createSocketTransport\n\nCreates a WebSocket/SockJS transport for real-time connections:\n\n```typescript\nimport { createSocketTransport } from '@octavus/react';\n\nconst transport = createSocketTransport({\n connect: () =>\n new Promise((resolve, reject) => {\n const ws = new WebSocket(`wss://api.example.com/stream?sessionId=${sessionId}`);\n ws.onopen = () => resolve(ws);\n ws.onerror = () => reject(new Error('Connection failed'));\n }),\n});\n```\n\nSocket transport provides additional connection management:\n\n```typescript\n// Access connection state directly\ntransport.connectionState; // 'disconnected' | 'connecting' | 'connected' | 'error'\n\n// Subscribe to state changes\ntransport.onConnectionStateChange((state, error) => {\n /* ... */\n});\n\n// Eager connection (instead of lazy on first send)\nawait transport.connect();\n\n// Manual disconnect\ntransport.disconnect();\n```\n\nFor detailed WebSocket/SockJS usage including custom events, reconnection patterns, and server-side implementation, see [Socket Transport](/docs/client-sdk/socket-transport).\n\n## Class Reference (Framework-Agnostic)\n\n### OctavusChat\n\n```typescript\nclass OctavusChat {\n constructor(options: OctavusChatOptions);\n\n // State (read-only)\n readonly messages: UIMessage[];\n readonly status: ChatStatus; // 'idle' | 'streaming' | 'error' | 'awaiting-input'\n readonly error: OctavusError | null; // Structured error\n readonly pendingClientTools: Record<string, InteractiveTool[]>; // Interactive tools\n\n // Actions\n send(\n triggerName: string,\n input?: Record<string, unknown>,\n options?: { userMessage?: UserMessageInput },\n ): Promise<void>;\n stop(): void;\n\n // Subscription\n subscribe(callback: () => void): () => void; // Returns unsubscribe function\n}\n```\n\n## Next Steps\n\n- [HTTP Transport](/docs/client-sdk/http-transport) - HTTP/SSE integration (recommended)\n- [Socket Transport](/docs/client-sdk/socket-transport) - WebSocket and SockJS integration\n- [Messages](/docs/client-sdk/messages) - Working with message state\n- [Streaming](/docs/client-sdk/streaming) - Building streaming UIs\n- [Client Tools](/docs/client-sdk/client-tools) - Interactive browser-side tool handling\n- [Operations](/docs/client-sdk/execution-blocks) - Showing agent progress\n- [Error Handling](/docs/client-sdk/error-handling) - Handling errors with type guards\n- [File Uploads](/docs/client-sdk/file-uploads) - Uploading images and documents\n- [Examples](/docs/examples/overview) - Complete working examples\n",
|
|
107
|
+
content: "\n# Client SDK Overview\n\nOctavus provides two packages for frontend integration:\n\n| Package | Purpose | Use When |\n| --------------------- | ------------------------ | ----------------------------------------------------- |\n| `@octavus/react` | React hooks and bindings | Building React applications |\n| `@octavus/client-sdk` | Framework-agnostic core | Using Vue, Svelte, vanilla JS, or custom integrations |\n\n**Most users should install `@octavus/react`** - it includes everything from `@octavus/client-sdk` plus React-specific hooks.\n\n## Installation\n\n### React Applications\n\n```bash\nnpm install @octavus/react\n```\n\n**Current version:** `3.5.0`\n\n### Other Frameworks\n\n```bash\nnpm install @octavus/client-sdk\n```\n\n**Current version:** `3.5.0`\n\n## Transport Pattern\n\nThe Client SDK uses a **transport abstraction** to handle communication with your backend. This gives you flexibility in how events are delivered:\n\n| Transport | Use Case | Docs |\n| ----------------------- | -------------------------------------------- | ----------------------------------------------------- |\n| `createHttpTransport` | HTTP/SSE (Next.js, Express, etc.) | [HTTP Transport](/docs/client-sdk/http-transport) |\n| `createSocketTransport` | WebSocket, SockJS, or other socket protocols | [Socket Transport](/docs/client-sdk/socket-transport) |\n\nWhen the transport changes (e.g., when `sessionId` changes), the `useOctavusChat` hook automatically reinitializes with the new transport.\n\n> **Recommendation**: Use HTTP transport unless you specifically need WebSocket features (custom real-time events, Meteor/Phoenix, etc.).\n\n## React Usage\n\nThe `useOctavusChat` hook provides state management and streaming for React applications:\n\n```tsx\nimport { useMemo } from 'react';\nimport { useOctavusChat, createHttpTransport, type UIMessage } from '@octavus/react';\n\nfunction Chat({ sessionId }: { sessionId: string }) {\n // Create a stable transport instance (memoized on sessionId)\n const transport = useMemo(\n () =>\n createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n }),\n [sessionId],\n );\n\n const { messages, status, send } = useOctavusChat({ transport });\n\n const sendMessage = async (text: string) => {\n await send('user-message', { USER_MESSAGE: text }, { userMessage: { content: text } });\n };\n\n return (\n <div>\n {messages.map((msg) => (\n <MessageBubble key={msg.id} message={msg} />\n ))}\n </div>\n );\n}\n\nfunction MessageBubble({ message }: { message: UIMessage }) {\n return (\n <div>\n {message.parts.map((part, i) => {\n if (part.type === 'text') {\n return <p key={i}>{part.text}</p>;\n }\n return null;\n })}\n </div>\n );\n}\n```\n\n## Framework-Agnostic Usage\n\nThe `OctavusChat` class can be used with any framework or vanilla JavaScript:\n\n```typescript\nimport { OctavusChat, createHttpTransport } from '@octavus/client-sdk';\n\nconst transport = createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n});\n\nconst chat = new OctavusChat({ transport });\n\n// Subscribe to state changes\nconst unsubscribe = chat.subscribe(() => {\n console.log('Messages:', chat.messages);\n console.log('Status:', chat.status);\n // Update your UI here\n});\n\n// Send a message\nawait chat.send('user-message', { USER_MESSAGE: 'Hello' }, { userMessage: { content: 'Hello' } });\n\n// Cleanup when done\nunsubscribe();\n```\n\n## Key Features\n\n### Unified Send Function\n\nThe `send` function handles both user message display and agent triggering in one call:\n\n```tsx\nconst { send } = useOctavusChat({ transport });\n\n// Add user message to UI and trigger agent\nawait send('user-message', { USER_MESSAGE: text }, { userMessage: { content: text } });\n\n// Trigger without adding a user message (e.g., button click)\nawait send('request-human');\n```\n\n### Message Parts\n\nMessages contain ordered `parts` for rich content:\n\n```tsx\nconst { messages } = useOctavusChat({ transport });\n\n// Each message has typed parts\nmessage.parts.map((part) => {\n switch (part.type) {\n case 'text': // Text content\n case 'reasoning': // Extended reasoning/thinking\n case 'tool-call': // Tool execution\n case 'operation': // Internal operations (set-resource, etc.)\n }\n});\n```\n\n### Status Tracking\n\n```tsx\nconst { status } = useOctavusChat({ transport });\n\n// status: 'idle' | 'streaming' | 'error' | 'awaiting-input'\n// 'awaiting-input' occurs when interactive client tools need user action\n```\n\n### Stop Streaming\n\n```tsx\nconst { stop } = useOctavusChat({ transport });\n\n// Stop current stream and finalize message\nstop();\n```\n\n### Retry Last Trigger\n\nRe-execute the last trigger from the same starting point. Messages are rolled back to the state before the trigger, the user message is re-added (if any), and the agent re-executes. Already-uploaded files are reused without re-uploading.\n\n```tsx\nconst { retry, canRetry } = useOctavusChat({ transport });\n\n// Retry after an error, cancellation, or unsatisfactory result\nif (canRetry) {\n await retry();\n}\n```\n\n`canRetry` is `true` when a trigger has been sent and the chat is not currently streaming or awaiting input.\n\n## Hook Reference (React)\n\n### useOctavusChat\n\n```typescript\nfunction useOctavusChat(options: OctavusChatOptions): UseOctavusChatReturn;\n\ninterface OctavusChatOptions {\n // Required: Transport for streaming events\n transport: Transport;\n\n // Optional: Function to request upload URLs for file uploads\n requestUploadUrls?: (\n files: { filename: string; mediaType: string; size: number }[],\n ) => Promise<UploadUrlsResponse>;\n\n // Optional: Client-side tool handlers\n // - Function: executes automatically and returns result\n // - 'interactive': appears in pendingClientTools for user input\n clientTools?: Record<string, ClientToolHandler>;\n\n // Optional: Pre-populate with existing messages (session restore)\n initialMessages?: UIMessage[];\n\n // Optional: Callbacks\n onError?: (error: OctavusError) => void; // Structured error with type, source, retryable\n onFinish?: () => void;\n onStop?: () => void; // Called when user stops generation\n onResourceUpdate?: (name: string, value: unknown) => void;\n}\n\ninterface UseOctavusChatReturn {\n // State\n messages: UIMessage[];\n status: ChatStatus; // 'idle' | 'streaming' | 'error' | 'awaiting-input'\n error: OctavusError | null; // Structured error with type, source, retryable\n\n // Connection (socket transport only - undefined for HTTP)\n connectionState: ConnectionState | undefined; // 'disconnected' | 'connecting' | 'connected' | 'error'\n connectionError: Error | undefined;\n\n // Client tools (interactive tools awaiting user input)\n pendingClientTools: Record<string, InteractiveTool[]>; // Keyed by tool name\n\n // Actions\n send: (\n triggerName: string,\n input?: Record<string, unknown>,\n options?: { userMessage?: UserMessageInput },\n ) => Promise<void>;\n stop: () => void;\n retry: () => Promise<void>; // Retry last trigger from same starting point\n canRetry: boolean; // Whether retry() can be called\n\n // Connection management (socket transport only - undefined for HTTP)\n connect: (() => Promise<void>) | undefined;\n disconnect: (() => void) | undefined;\n\n // File uploads (requires requestUploadUrls)\n uploadFiles: (\n files: FileList | File[],\n onProgress?: (fileIndex: number, progress: number) => void,\n ) => Promise<FileReference[]>;\n}\n\ninterface UserMessageInput {\n content?: string;\n files?: FileList | File[] | FileReference[];\n}\n```\n\n### useAutoScroll\n\nSmart auto-scroll for chat containers. Scrolls to bottom when content updates, but pauses if the user has scrolled up. See [Streaming - Auto-Scroll](/docs/client-sdk/streaming#auto-scroll) for full usage.\n\n```typescript\nfunction useAutoScroll(options?: UseAutoScrollOptions): {\n scrollRef: RefObject<HTMLDivElement | null>;\n handleScroll: () => void;\n scrollOnUpdate: () => void;\n resetAutoScroll: () => void;\n};\n\ninterface UseAutoScrollOptions {\n scrollRef?: RefObject<HTMLDivElement | null>;\n threshold?: number; // Distance from bottom in px (default: 80)\n}\n```\n\n## Transport Reference\n\n### createHttpTransport\n\nCreates an HTTP/SSE transport using native `fetch()`:\n\n```typescript\nimport { createHttpTransport } from '@octavus/react';\n\nconst transport = createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n});\n```\n\n### createSocketTransport\n\nCreates a WebSocket/SockJS transport for real-time connections:\n\n```typescript\nimport { createSocketTransport } from '@octavus/react';\n\nconst transport = createSocketTransport({\n connect: () =>\n new Promise((resolve, reject) => {\n const ws = new WebSocket(`wss://api.example.com/stream?sessionId=${sessionId}`);\n ws.onopen = () => resolve(ws);\n ws.onerror = () => reject(new Error('Connection failed'));\n }),\n});\n```\n\nSocket transport provides additional connection management:\n\n```typescript\n// Access connection state directly\ntransport.connectionState; // 'disconnected' | 'connecting' | 'connected' | 'error'\n\n// Subscribe to state changes\ntransport.onConnectionStateChange((state, error) => {\n /* ... */\n});\n\n// Eager connection (instead of lazy on first send)\nawait transport.connect();\n\n// Manual disconnect\ntransport.disconnect();\n```\n\nFor detailed WebSocket/SockJS usage including custom events, reconnection patterns, and server-side implementation, see [Socket Transport](/docs/client-sdk/socket-transport).\n\n## Class Reference (Framework-Agnostic)\n\n### OctavusChat\n\n```typescript\nclass OctavusChat {\n constructor(options: OctavusChatOptions);\n\n // State (read-only)\n readonly messages: UIMessage[];\n readonly status: ChatStatus; // 'idle' | 'streaming' | 'error' | 'awaiting-input'\n readonly error: OctavusError | null; // Structured error\n readonly pendingClientTools: Record<string, InteractiveTool[]>; // Interactive tools\n\n // Actions\n send(\n triggerName: string,\n input?: Record<string, unknown>,\n options?: { userMessage?: UserMessageInput },\n ): Promise<void>;\n stop(): void;\n\n // Subscription\n subscribe(callback: () => void): () => void; // Returns unsubscribe function\n}\n```\n\n## Next Steps\n\n- [HTTP Transport](/docs/client-sdk/http-transport) - HTTP/SSE integration (recommended)\n- [Socket Transport](/docs/client-sdk/socket-transport) - WebSocket and SockJS integration\n- [Messages](/docs/client-sdk/messages) - Working with message state\n- [Streaming](/docs/client-sdk/streaming) - Building streaming UIs\n- [Client Tools](/docs/client-sdk/client-tools) - Interactive browser-side tool handling\n- [Operations](/docs/client-sdk/execution-blocks) - Showing agent progress\n- [Error Handling](/docs/client-sdk/error-handling) - Handling errors with type guards\n- [File Uploads](/docs/client-sdk/file-uploads) - Uploading images and documents\n- [Examples](/docs/examples/overview) - Complete working examples\n",
|
|
99
108
|
excerpt: "Client SDK Overview Octavus provides two packages for frontend integration: | Package | Purpose | Use When | |...",
|
|
100
109
|
order: 1
|
|
101
110
|
},
|
|
@@ -660,8 +669,8 @@ See [Streaming Events](/docs/server-sdk/streaming#event-types) for the full list
|
|
|
660
669
|
section: "protocol",
|
|
661
670
|
title: "MCP Servers",
|
|
662
671
|
description: "Connecting agents to external tools via Model Context Protocol (MCP).",
|
|
663
|
-
content: "\n# MCP Servers\n\nMCP servers extend your agent with tools from external services. Define them in your protocol, and agents automatically discover and use their tools at runtime.\n\nThere are two types of MCP servers:\n\n| Source | Description | Example |\n| -------- | ------------------------------------------------------------------ | ------------------------------ |\n| `remote` | HTTP-based MCP servers, managed by the platform | Figma, Sentry, GitHub |\n| `device` | Local MCP servers running on the consumer's machine via server-sdk | Browser automation, filesystem |\n\n## Defining MCP Servers\n\nMCP servers are defined in the `mcpServers:` section. The key becomes the **namespace** for all tools from that server.\n\n```yaml\nmcpServers:\n figma:\n description: Figma design tool integration\n source: remote\n display: description\n\n browser:\n description: Chrome DevTools browser automation\n source: device\n display: name\n```\n\n### Fields\n\n| Field | Required | Description |\n| ------------- | -------- | ------------------------------------------------------------------------------------------------------- |\n| `description` | Yes | What the MCP server provides |\n| `source` | Yes | `remote` (platform-managed) or `device` (consumer-provided) |\n| `display` | No | How tool calls appear in UI: `hidden`, `name`, `description` (default: `description`) |\n| `connection` | No | When to connect: `eager` or `lazy` (default: `lazy`). Remote only. |\n| `execution` | No | Where the MCP process runs: `sandbox` (default) or `device`. See [Device Execution](#device-execution). |\n\n### Display Modes\n\nDisplay modes control visibility of all tool calls from the MCP server, using the same modes as [regular tools](/docs/protocol/tools#display-modes):\n\n| Mode | Behavior |\n| ------------- | -------------------------------------- |\n| `hidden` | Tool calls run silently |\n| `name` | Shows tool name while executing |\n| `description` | Shows tool description while executing |\n\n## Making MCP Servers Available\n\nLike tools, MCP servers defined in `mcpServers:` must be referenced in `agent.mcpServers` to be available:\n\n```yaml\nmcpServers:\n figma:\n description: Figma design tool integration\n source: remote\n display: description\n\n sentry:\n description: Error tracking and debugging\n source: remote\n display: name\n\n browser:\n description: Chrome DevTools browser automation\n source: device\n display: name\n\n filesystem:\n description: Filesystem access for reading and writing files\n source: device\n display: hidden\n\nagent:\n model: anthropic/claude-sonnet-4-5\n system: system\n mcpServers: [figma, sentry, browser, filesystem]\n tools: [set-chat-title]\n agentic: true\n maxSteps: 100\n```\n\n## Tool Namespacing\n\nAll MCP tools are automatically namespaced using `__` (double underscore) as a separator. The namespace comes from the `mcpServers` key.\n\nFor example, a server defined as `browser:` that exposes `navigate_page` and `click` produces:\n\n- `browser__navigate_page`\n- `browser__click`\n\nA server defined as `figma:` that exposes `get_design_context` produces:\n\n- `figma__get_design_context`\n\nThe namespace is stripped before calling the MCP server - the server receives the original tool name. This convention matches Anthropic's MCP integration in Claude Desktop and ensures tool names stay unique across servers.\n\n### What the LLM Sees\n\nWhen an agent has both regular tools and MCP servers configured, the LLM sees all tools combined:\n\n```\nProtocol tools:\n set-chat-title\n\nRemote MCP tools (auto-discovered):\n figma__get_design_context\n figma__get_screenshot\n sentry__get_issues\n sentry__get_issue_details\n\nDevice MCP tools (auto-discovered):\n browser__navigate_page\n browser__click\n browser__take_snapshot\n filesystem__read_file\n filesystem__write_file\n filesystem__list_directory\n```\n\nYou don't define individual MCP tool schemas in the protocol - they're auto-discovered from each MCP server at runtime.\n\n## Remote MCP Servers\n\nRemote MCP servers (`source: remote`) connect to HTTP-based MCP endpoints. The platform manages the connection, authentication, and tool discovery.\n\nConfiguration happens in the Octavus platform UI:\n\n1. Add an MCP server to your project (URL + authentication)\n2. The server's slug must match the namespace in your protocol\n3. The platform connects, discovers tools, and makes them available to the agent\n\n### Connection Modes\n\nThe `connection` field controls when the platform connects to a remote MCP server:\n\n| Mode | Behavior |\n| ------- | ---------------------------------------------------------------------------------------------------------------------- |\n| `lazy` | (default) The agent activates integrations on demand at runtime. The agent starts responding immediately. |\n| `eager` | The platform connects and discovers tools before the first LLM request. Tools are guaranteed available from message 1. |\n\n```yaml\nmcpServers:\n sentry:\n source: remote\n connection: eager # Always connected upfront\n display: name\n\n notion:\n source: remote\n # connection defaults to lazy - agent activates when needed\n display: description\n```\n\nWith **lazy connection** (the default), the agent receives two built-in tools - one for listing available integrations and one for activating them. The agent decides which integrations it needs based on the conversation and activates them on demand. This avoids paying connection latency for integrations the agent doesn't end up using.\n\nWith **eager connection**, the platform connects to the MCP server before the first LLM request, exactly like a declared tool. Use this when the agent needs the MCP's tools from the very first message.\n\nThe `connection` field is only valid on `source: remote` - device MCPs (`source: device`) have their own connection mechanism through the server-sdk. The `connection` field is respected for remote MCPs with `execution: device` the same way as sandbox MCPs.\n\n### Authentication\n\nRemote MCP servers support multiple authentication methods:\n\n| Auth Type | Description |\n| --------- | ------------------------------- |\n| MCP OAuth | Standard MCP OAuth flow |\n| API Key | Static API key sent as a header |\n| Bearer | Bearer token authentication |\n| None | No authentication required |\n\nAuthentication is configured per-project - different projects can connect to the same MCP server with different credentials.\n\n## Device Execution\n\nThe `execution` field controls where a remote MCP server's STDIO process runs. By default (`execution: sandbox`), the process runs in the platform's sandbox. When set to `execution: device`, the STDIO process runs on the agent's computer (VM or desktop) instead.\n\n```yaml\nmcpServers:\n code-tools:\n description: Code analysis and refactoring tools\n source: remote\n execution: device # STDIO process runs on the agent's computer\n display: name\n\n sentry:\n description: Error tracking\n source: remote\n # execution defaults to sandbox - runs in the platform\n display: name\n```\n\n### When to Use\n\nUse `execution: device` when the MCP server needs access to the agent's local environment - for example, tools that read from the local filesystem, interact with running processes, or need CLIs installed on the device.\n\n### Rules\n\n- `execution` is only meaningful for `source: remote` MCPs that use STDIO transport. HTTP-transport remote MCPs always connect from the platform regardless of the `execution` setting.\n- `execution: device` is **invalid** on `source: device` MCPs (they already run on the device by definition). Using it produces a validation error.\n- The `connection` field (`eager` or `lazy`) is respected for device-executed MCPs the same way as sandbox-executed MCPs.\n\n## Device MCP Servers\n\nDevice MCP servers (`source: device`) run on the consumer's machine. The consumer provides the MCP tools via the `@octavus/computer` package (or any `ToolProvider` implementation) through the server-sdk.\n\nWhen an agent has device MCP servers:\n\n1. The consumer creates a `Computer` with matching namespaces\n2. `@octavus/computer` discovers tools from each MCP server\n3. Tool schemas are sent to the platform via the server-sdk\n4. Tool calls flow back to the consumer for execution\n\nSee [`@octavus/computer`](/docs/server-sdk/computer) for the full integration guide.\n\n### Namespace Matching\n\nThe `mcpServers` keys in the protocol must match the keys in the consumer's `Computer` configuration:\n\n```yaml\n# protocol.yaml\nmcpServers:\n browser: # \u2190 must match\n source: device\n filesystem: # \u2190 must match\n source: device\n```\n\n```typescript\nconst computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', ['--browser-url=...']),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', [dir]),\n },\n});\n```\n\nIf the consumer provides a namespace not declared in the protocol, the platform rejects it.\n\n## Thread-Level Scoping\n\nThreads can scope which MCP servers are available, the same way they scope [tools](/docs/protocol/handlers#start-thread):\n\n```yaml\nhandlers:\n user-message:\n Start research:\n block: start-thread\n thread: research\n mcpServers: [figma, browser]\n tools: [set-chat-title]\n system: research-prompt\n```\n\nThis thread can use Figma and browser tools, but not sentry or filesystem - even if those are available on the main agent.\n\n## On-Demand MCP Servers\n\nBy default, an agent can only call MCP tools whose namespace is listed in `mcpServers`. With `onDemandMcpServers`, a scope can opt into **every connected MCP of a given source** at runtime, without enumerating each one in the protocol.\n\nRemote MCPs are connected at the project level from the Octavus dashboard. Normally, each connected MCP that the agent should be able to use has to be declared in the protocol - connecting a new MCP means editing the protocol and redeploying. `onDemandMcpServers` removes that round-trip: once a source is opted in, any MCP connected to the project under that source becomes available to the agent immediately.\n\nCurrently supported for `source: remote`.\n\n### Protocol-level declaration\n\nAdd an `onDemandMcpServers:` section alongside `mcpServers:`, keyed by source. Each entry configures how the matched MCPs appear in tool lists:\n\n```yaml\nmcpServers:\n figma:\n description: Figma design tool integration\n source: remote\n display: description\n\nonDemandMcpServers:\n remote:\n description: Additional connected integrations\n display: name\n execution: device # on-demand MCPs run on the agent's computer\n contextRetention:\n toolResults: { retainLast: 5 }\n```\n\nOn-demand MCP definitions also support the `execution` field. When set, all MCPs matched by that on-demand source inherit the execution mode.\n\n### Scope-level opt-in\n\nThe agent and individual `start-thread` blocks each choose whether to pick up on-demand MCPs, by listing the sources they want:\n\n```yaml\nagent:\n mcpServers: [figma]\n onDemandMcpServers: [remote]\n\nhandlers:\n user-message:\n focused:\n block: start-thread\n mcpServers: [figma]\n # no onDemandMcpServers - this thread does NOT see on-demand MCPs\n broad:\n block: start-thread\n mcpServers: [figma]\n onDemandMcpServers: [remote]\n```\n\n### Rules\n\n- A scope's tool list includes every **connected** MCP of any referenced source, whether or not any protocol declares that slug.\n- Undeclared namespaces inherit `description`, `display`, and `contextRetention` from the per-source entry in `onDemandMcpServers`.\n- Scopes decide independently - threads do not inherit `onDemandMcpServers` from their parent, the same rule as `mcpServers:`.\n- Tool namespaces are always the connector's slug (for example `notion__search`, `linear__create_issue`). Source keys are never namespaces.\n\nWorkers opt into on-demand MCPs the same way: through `start-thread` blocks inside `steps`. A worker without a `start-thread` that lists a source won't see on-demand MCPs of that source.\n\n## Workers\n\nWorkers can declare and use MCP servers using the same `mcpServers:` syntax. Workers resolve their own MCP connections independently - they don't inherit from a parent interactive agent.\n\n```yaml\n# Worker protocol\nmcpServers:\n sentry:\n description: Error tracking and debugging\n source: remote\n display: name\n browser:\n description: Chrome DevTools browser automation\n source: device\n display: name\n\nsteps:\n Start research:\n block: start-thread\n thread: research\n model: anthropic/claude-sonnet-4-5\n system: system\n mcpServers: [sentry, browser]\n maxSteps: 10\n```\n\nSince workers don't have a global `agent:` section, MCP servers are scoped per-thread via `start-thread` - the same way tools and skills work in workers. Remote MCP connections are project-scoped, so workers in the same project share the same OAuth connections.\n\nSee [Workers](/docs/protocol/workers) for the full worker protocol reference.\n\n## Full Example\n\n```yaml\nmcpServers:\n figma:\n description: Figma design tool integration\n source: remote\n connection: eager\n display: description\n sentry:\n description: Error tracking and debugging\n source: remote\n display: name\n browser:\n description: Chrome DevTools browser automation\n source: device\n display: name\n filesystem:\n description: Filesystem access for reading and writing files\n source: device\n display: hidden\n shell:\n description: Shell command execution\n source: device\n display: name\n\ntools:\n set-chat-title:\n description: Set the title of the current chat.\n parameters:\n title: { type: string, description: The title to set }\n\nagent:\n model: anthropic/claude-opus-4-6\n system: system\n mcpServers: [figma, sentry, browser, filesystem, shell]\n tools: [set-chat-title]\n thinking: medium\n maxSteps: 300\n agentic: true\n\ntriggers:\n user-message:\n input:\n USER_MESSAGE: { type: string }\n\nhandlers:\n user-message:\n Add message:\n block: add-message\n role: user\n prompt: user-message\n input: [USER_MESSAGE]\n display: hidden\n\n Respond:\n block: next-message\n```\n\n### Cloud-Only Agent\n\nAgents that only use remote MCP servers don't need `@octavus/computer`:\n\n```yaml\nmcpServers:\n figma:\n description: Figma design tool integration\n source: remote\n connection: eager # Need design tools from message 1\n display: description\n sentry:\n description: Error tracking and debugging\n source: remote\n # Lazy (default) - agent activates when debugging is needed\n display: name\n\ntools:\n submit-code:\n description: Submit code to the user.\n parameters:\n code: { type: string }\n\nagent:\n model: anthropic/claude-sonnet-4-5\n system: system\n mcpServers: [figma, sentry]\n tools: [submit-code]\n agentic: true\n```\n",
|
|
664
|
-
excerpt: "MCP Servers MCP servers extend your agent with tools from external services. Define them in your protocol, and agents automatically discover and use their tools at runtime. There are
|
|
672
|
+
content: "\n# MCP Servers\n\nMCP servers extend your agent with tools from external services. Define them in your protocol, and agents automatically discover and use their tools at runtime.\n\nThere are three types of MCP servers:\n\n| Source | Description | Example |\n| ---------- | ----------------------------------------------------------------------------------- | ------------------------------------- |\n| `remote` | HTTP-based MCP servers, managed by the platform | Figma, Sentry, GitHub |\n| `device` | Local MCP servers running on the agent's machine via `@octavus/computer` | Browser automation, filesystem |\n| `consumer` | Inline MCP servers defined in your server-sdk process via `createInlineMcpServer()` | Custom integrations, third-party APIs |\n\n## Defining MCP Servers\n\nMCP servers are defined in the `mcpServers:` section. The key becomes the **namespace** for all tools from that server.\n\n```yaml\nmcpServers:\n figma:\n description: Figma design tool integration\n source: remote\n display: description\n\n browser:\n description: Chrome DevTools browser automation\n source: device\n display: name\n\n github:\n description: Repository management - issues, pull requests, code\n source: consumer\n display: name\n```\n\n### Fields\n\n| Field | Required | Description |\n| ------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |\n| `description` | Yes | What the MCP server provides |\n| `source` | Yes | `remote`, `device`, or `consumer` (see source types above) |\n| `display` | No | How tool calls appear in UI: `hidden`, `name`, `description` (default: `description`) |\n| `connection` | No | When to connect: `eager` or `lazy` (default: `lazy`). `remote` only. |\n| `execution` | No | Where the MCP process runs: `sandbox` (default) or `device`. `remote` only. See [Device Execution](#device-execution). |\n\n### Display Modes\n\nDisplay modes control visibility of all tool calls from the MCP server, using the same modes as [regular tools](/docs/protocol/tools#display-modes):\n\n| Mode | Behavior |\n| ------------- | -------------------------------------- |\n| `hidden` | Tool calls run silently |\n| `name` | Shows tool name while executing |\n| `description` | Shows tool description while executing |\n\n## Making MCP Servers Available\n\nLike tools, MCP servers defined in `mcpServers:` must be referenced in `agent.mcpServers` to be available:\n\n```yaml\nmcpServers:\n figma:\n description: Figma design tool integration\n source: remote\n display: description\n\n sentry:\n description: Error tracking and debugging\n source: remote\n display: name\n\n browser:\n description: Chrome DevTools browser automation\n source: device\n display: name\n\n filesystem:\n description: Filesystem access for reading and writing files\n source: device\n display: hidden\n\nagent:\n model: anthropic/claude-sonnet-4-5\n system: system\n mcpServers: [figma, sentry, browser, filesystem]\n tools: [set-chat-title]\n agentic: true\n maxSteps: 100\n```\n\n## Tool Namespacing\n\nAll MCP tools are automatically namespaced using `__` (double underscore) as a separator. The namespace comes from the `mcpServers` key.\n\nFor example, a server defined as `browser:` that exposes `navigate_page` and `click` produces:\n\n- `browser__navigate_page`\n- `browser__click`\n\nA server defined as `figma:` that exposes `get_design_context` produces:\n\n- `figma__get_design_context`\n\nThe namespace is stripped before calling the MCP server - the server receives the original tool name. This convention matches Anthropic's MCP integration in Claude Desktop and ensures tool names stay unique across servers.\n\n### What the LLM Sees\n\nWhen an agent has both regular tools and MCP servers configured, the LLM sees all tools combined:\n\n```\nProtocol tools:\n set-chat-title\n\nRemote MCP tools (auto-discovered):\n figma__get_design_context\n figma__get_screenshot\n sentry__get_issues\n sentry__get_issue_details\n\nDevice MCP tools (auto-discovered):\n browser__navigate_page\n browser__click\n browser__take_snapshot\n filesystem__read_file\n filesystem__write_file\n filesystem__list_directory\n\nConsumer MCP tools (provided by the server-sdk):\n github__get-pr-overview\n github__list-issues\n```\n\nYou don't define individual MCP tool schemas in the protocol - remote and device tools are auto-discovered from each MCP server at runtime, and consumer tools are supplied by the server-sdk.\n\n## Remote MCP Servers\n\nRemote MCP servers (`source: remote`) connect to HTTP-based MCP endpoints. The platform manages the connection, authentication, and tool discovery.\n\nConfiguration happens in the Octavus platform UI:\n\n1. Add an MCP server to your project (URL + authentication)\n2. The server's slug must match the namespace in your protocol\n3. The platform connects, discovers tools, and makes them available to the agent\n\n### Connection Modes\n\nThe `connection` field controls when the platform connects to a remote MCP server:\n\n| Mode | Behavior |\n| ------- | ---------------------------------------------------------------------------------------------------------------------- |\n| `lazy` | (default) The agent activates integrations on demand at runtime. The agent starts responding immediately. |\n| `eager` | The platform connects and discovers tools before the first LLM request. Tools are guaranteed available from message 1. |\n\n```yaml\nmcpServers:\n sentry:\n source: remote\n connection: eager # Always connected upfront\n display: name\n\n notion:\n source: remote\n # connection defaults to lazy - agent activates when needed\n display: description\n```\n\nWith **lazy connection** (the default), the agent receives two built-in tools - one for listing available integrations and one for activating them. The agent decides which integrations it needs based on the conversation and activates them on demand. This avoids paying connection latency for integrations the agent doesn't end up using.\n\nWith **eager connection**, the platform connects to the MCP server before the first LLM request, exactly like a declared tool. Use this when the agent needs the MCP's tools from the very first message.\n\nThe `connection` field is only valid on `source: remote` - device MCPs (`source: device`) have their own connection mechanism through the server-sdk. The `connection` field is respected for remote MCPs with `execution: device` the same way as sandbox MCPs.\n\n### Authentication\n\nRemote MCP servers support multiple authentication methods:\n\n| Auth Type | Description |\n| --------- | ------------------------------- |\n| MCP OAuth | Standard MCP OAuth flow |\n| API Key | Static API key sent as a header |\n| Bearer | Bearer token authentication |\n| None | No authentication required |\n\nAuthentication is configured per-project - different projects can connect to the same MCP server with different credentials.\n\n## Device Execution\n\nThe `execution` field controls where a remote MCP server's STDIO process runs. By default (`execution: sandbox`), the process runs in the platform's sandbox. When set to `execution: device`, the STDIO process runs on the agent's computer (VM or desktop) instead.\n\n```yaml\nmcpServers:\n code-tools:\n description: Code analysis and refactoring tools\n source: remote\n execution: device # STDIO process runs on the agent's computer\n display: name\n\n sentry:\n description: Error tracking\n source: remote\n # execution defaults to sandbox - runs in the platform\n display: name\n```\n\n### When to Use\n\nUse `execution: device` when the MCP server needs access to the agent's local environment - for example, tools that read from the local filesystem, interact with running processes, or need CLIs installed on the device.\n\n### Rules\n\n- `execution` is only meaningful for `source: remote` MCPs that use STDIO transport. HTTP-transport remote MCPs always connect from the platform regardless of the `execution` setting.\n- `execution` is **invalid** on `source: device` and `source: consumer` MCPs - they already run outside the platform. Using it produces a validation error.\n- The `connection` field (`eager` or `lazy`) is respected for device-executed MCPs the same way as sandbox-executed MCPs.\n\n## Device MCP Servers\n\nDevice MCP servers (`source: device`) run on the consumer's machine. The consumer provides the MCP tools via the `@octavus/computer` package (or any `ToolProvider` implementation) through the server-sdk.\n\nWhen an agent has device MCP servers:\n\n1. The consumer creates a `Computer` with matching namespaces\n2. `@octavus/computer` discovers tools from each MCP server\n3. Tool schemas are sent to the platform via the server-sdk\n4. Tool calls flow back to the consumer for execution\n\nSee [`@octavus/computer`](/docs/server-sdk/computer) for the full integration guide.\n\n### Namespace Matching\n\nThe `mcpServers` keys in the protocol must match the keys in the consumer's `Computer` configuration:\n\n```yaml\n# protocol.yaml\nmcpServers:\n browser: # \u2190 must match\n source: device\n filesystem: # \u2190 must match\n source: device\n```\n\n```typescript\nconst computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', ['--browser-url=...']),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', [dir]),\n },\n});\n```\n\nIf the consumer provides a namespace not declared in the protocol, the platform rejects it.\n\n## Consumer MCP Servers\n\nConsumer MCP servers (`source: consumer`) are defined inline in your server-sdk process. Tool schemas and handlers live in your code; the platform learns the namespace from the protocol and routes tool calls to your process via the same `dynamicToolSchemas` channel that device MCPs use.\n\n```yaml\nmcpServers:\n github:\n description: Repository management - issues, pull requests, code\n source: consumer\n display: name\n\nagent:\n mcpServers:\n - github\n```\n\nThe protocol declaration is intentionally minimal - the SDK supplies tool names and JSON schemas at runtime, so adding or evolving tools doesn't require a protocol change.\n\nUse consumer MCPs when:\n\n- The integration's credentials should never reach the platform (OAuth tokens, customer API keys).\n- You want to group an integration's tools (`github__list-prs`, `github__get-issue`) without enumerating each one in YAML.\n- You want type-safe handler arguments via Zod schemas.\n\nSee [`createInlineMcpServer`](/docs/server-sdk/inline-mcp) in the server-sdk reference for the full implementation guide.\n\n### Namespace Matching\n\nThe protocol namespace must match the namespace passed to `createInlineMcpServer()`:\n\n```yaml\n# protocol.yaml\nmcpServers:\n github: # \u2190 must match\n source: consumer\n```\n\n```typescript\nconst github = createInlineMcpServer('github', {\n /* tools... */\n});\n\nsession = client.agentSessions.attach(sessionId, {\n mcpServers: [github],\n});\n```\n\nIf the SDK provides a namespace not declared in the protocol, those tools are filtered out at the runtime boundary and the LLM never sees them.\n\n## Thread-Level Scoping\n\nThreads can scope which MCP servers are available, the same way they scope [tools](/docs/protocol/handlers#start-thread):\n\n```yaml\nhandlers:\n user-message:\n Start research:\n block: start-thread\n thread: research\n mcpServers: [figma, browser]\n tools: [set-chat-title]\n system: research-prompt\n```\n\nThis thread can use Figma and browser tools, but not sentry or filesystem - even if those are available on the main agent.\n\n## On-Demand MCP Servers\n\nBy default, an agent can only call MCP tools whose namespace is listed in `mcpServers`. With `onDemandMcpServers`, a scope can opt into **every connected MCP of a given source** at runtime, without enumerating each one in the protocol.\n\nRemote MCPs are connected at the project level from the Octavus dashboard. Normally, each connected MCP that the agent should be able to use has to be declared in the protocol - connecting a new MCP means editing the protocol and redeploying. `onDemandMcpServers` removes that round-trip: once a source is opted in, any MCP connected to the project under that source becomes available to the agent immediately.\n\nCurrently supported for `source: remote`.\n\n### Protocol-level declaration\n\nAdd an `onDemandMcpServers:` section alongside `mcpServers:`, keyed by source. Each entry configures how the matched MCPs appear in tool lists:\n\n```yaml\nmcpServers:\n figma:\n description: Figma design tool integration\n source: remote\n display: description\n\nonDemandMcpServers:\n remote:\n description: Additional connected integrations\n display: name\n execution: device # on-demand MCPs run on the agent's computer\n contextRetention:\n toolResults: { retainLast: 5 }\n```\n\nOn-demand MCP definitions also support the `execution` field. When set, all MCPs matched by that on-demand source inherit the execution mode.\n\n### Scope-level opt-in\n\nThe agent and individual `start-thread` blocks each choose whether to pick up on-demand MCPs, by listing the sources they want:\n\n```yaml\nagent:\n mcpServers: [figma]\n onDemandMcpServers: [remote]\n\nhandlers:\n user-message:\n focused:\n block: start-thread\n mcpServers: [figma]\n # no onDemandMcpServers - this thread does NOT see on-demand MCPs\n broad:\n block: start-thread\n mcpServers: [figma]\n onDemandMcpServers: [remote]\n```\n\n### Rules\n\n- A scope's tool list includes every **connected** MCP of any referenced source, whether or not any protocol declares that slug.\n- Undeclared namespaces inherit `description`, `display`, and `contextRetention` from the per-source entry in `onDemandMcpServers`.\n- Scopes decide independently - threads do not inherit `onDemandMcpServers` from their parent, the same rule as `mcpServers:`.\n- Tool namespaces are always the connector's slug (for example `notion__search`, `linear__create_issue`). Source keys are never namespaces.\n\nWorkers opt into on-demand MCPs the same way: through `start-thread` blocks inside `steps`. A worker without a `start-thread` that lists a source won't see on-demand MCPs of that source.\n\n## Workers\n\nWorkers can declare and use MCP servers using the same `mcpServers:` syntax. Workers resolve their own MCP connections independently - they don't inherit from a parent interactive agent.\n\n```yaml\n# Worker protocol\nmcpServers:\n sentry:\n description: Error tracking and debugging\n source: remote\n display: name\n browser:\n description: Chrome DevTools browser automation\n source: device\n display: name\n\nsteps:\n Start research:\n block: start-thread\n thread: research\n model: anthropic/claude-sonnet-4-5\n system: system\n mcpServers: [sentry, browser]\n maxSteps: 10\n```\n\nSince workers don't have a global `agent:` section, MCP servers are scoped per-thread via `start-thread` - the same way tools and skills work in workers. Remote MCP connections are project-scoped, so workers in the same project share the same OAuth connections.\n\nSee [Workers](/docs/protocol/workers) for the full worker protocol reference.\n\n## Full Example\n\n```yaml\nmcpServers:\n figma:\n description: Figma design tool integration\n source: remote\n connection: eager\n display: description\n sentry:\n description: Error tracking and debugging\n source: remote\n display: name\n browser:\n description: Chrome DevTools browser automation\n source: device\n display: name\n filesystem:\n description: Filesystem access for reading and writing files\n source: device\n display: hidden\n shell:\n description: Shell command execution\n source: device\n display: name\n\ntools:\n set-chat-title:\n description: Set the title of the current chat.\n parameters:\n title: { type: string, description: The title to set }\n\nagent:\n model: anthropic/claude-opus-4-6\n system: system\n mcpServers: [figma, sentry, browser, filesystem, shell]\n tools: [set-chat-title]\n thinking: medium\n maxSteps: 300\n agentic: true\n\ntriggers:\n user-message:\n input:\n USER_MESSAGE: { type: string }\n\nhandlers:\n user-message:\n Add message:\n block: add-message\n role: user\n prompt: user-message\n input: [USER_MESSAGE]\n display: hidden\n\n Respond:\n block: next-message\n```\n\n### Cloud-Only Agent\n\nAgents that only use remote MCP servers don't need `@octavus/computer`:\n\n```yaml\nmcpServers:\n figma:\n description: Figma design tool integration\n source: remote\n connection: eager # Need design tools from message 1\n display: description\n sentry:\n description: Error tracking and debugging\n source: remote\n # Lazy (default) - agent activates when debugging is needed\n display: name\n\ntools:\n submit-code:\n description: Submit code to the user.\n parameters:\n code: { type: string }\n\nagent:\n model: anthropic/claude-sonnet-4-5\n system: system\n mcpServers: [figma, sentry]\n tools: [submit-code]\n agentic: true\n```\n",
|
|
673
|
+
excerpt: "MCP Servers MCP servers extend your agent with tools from external services. Define them in your protocol, and agents automatically discover and use their tools at runtime. There are three types of...",
|
|
665
674
|
order: 13
|
|
666
675
|
},
|
|
667
676
|
{
|
|
@@ -759,7 +768,7 @@ var sections_default = [
|
|
|
759
768
|
section: "server-sdk",
|
|
760
769
|
title: "Overview",
|
|
761
770
|
description: "Introduction to the Octavus Server SDK for backend integration.",
|
|
762
|
-
content: "\n# Server SDK Overview\n\nThe `@octavus/server-sdk` package provides a Node.js SDK for integrating Octavus agents into your backend application. It handles session management, streaming, and the tool execution continuation loop.\n\n**Current version:** `3.
|
|
771
|
+
content: "\n# Server SDK Overview\n\nThe `@octavus/server-sdk` package provides a Node.js SDK for integrating Octavus agents into your backend application. It handles session management, streaming, and the tool execution continuation loop.\n\n**Current version:** `3.5.0`\n\n## Installation\n\n```bash\nnpm install @octavus/server-sdk\n```\n\nFor agent management (sync, validate), install the CLI as a dev dependency:\n\n```bash\nnpm install --save-dev @octavus/cli\n```\n\n## Basic Usage\n\n```typescript\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: 'https://octavus.ai',\n apiKey: 'your-api-key',\n});\n```\n\n## Key Features\n\n### Agent Management\n\nAgent definitions are managed via the CLI. See the [CLI documentation](/docs/server-sdk/cli) for details.\n\n```bash\n# Sync agent from local files\noctavus sync ./agents/support-chat\n\n# Output: Created: support-chat\n# Agent ID: clxyz123abc456\n```\n\n### Session Management\n\nCreate and manage agent sessions using the agent ID:\n\n```typescript\n// Create a new session (use agent ID from CLI sync)\nconst sessionId = await client.agentSessions.create('clxyz123abc456', {\n COMPANY_NAME: 'Acme Corp',\n PRODUCT_NAME: 'Widget Pro',\n});\n\n// Get UI-ready session messages (for session restore)\nconst session = await client.agentSessions.getMessages(sessionId);\n```\n\n### Tool Handlers\n\nTools run on your server with your data:\n\n```typescript\nconst session = client.agentSessions.attach(sessionId, {\n tools: {\n 'get-user-account': async (args) => {\n // Access your database, APIs, etc.\n return await db.users.findById(args.userId);\n },\n },\n});\n```\n\n### Streaming\n\nAll responses stream in real-time:\n\n```typescript\nimport { toSSEStream } from '@octavus/server-sdk';\n\n// execute() returns an async generator of events\nconst events = session.execute({\n type: 'trigger',\n triggerName: 'user-message',\n input: { USER_MESSAGE: 'Hello!' },\n});\n\n// Convert to SSE stream for HTTP responses\nreturn new Response(toSSEStream(events), {\n headers: { 'Content-Type': 'text/event-stream' },\n});\n```\n\n### Computer Capabilities\n\nGive agents access to browser, filesystem, and shell via MCP:\n\n```typescript\nimport { Computer } from '@octavus/computer';\n\nconst computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', ['--browser-url=...']),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', [dir]),\n shell: Computer.shell({ cwd: dir, mode: 'unrestricted' }),\n },\n});\n\nawait computer.start();\n\nconst session = client.agentSessions.attach(sessionId, {\n tools: {\n 'set-chat-title': async (args) => ({ title: args.title }),\n },\n});\n\nsession.setDynamicTools(computer);\n```\n\n### Workers\n\nExecute worker agents for task-based processing:\n\n```typescript\n// Non-streaming: get the output directly\nconst { output } = await client.workers.generate(agentId, {\n TOPIC: 'AI safety',\n});\n\n// Streaming: observe events in real-time\nfor await (const event of client.workers.execute(agentId, input)) {\n // Handle stream events\n}\n```\n\n## API Reference\n\n### OctavusClient\n\nThe main entry point for interacting with Octavus.\n\n```typescript\ninterface OctavusClientConfig {\n baseUrl: string; // Octavus API URL\n apiKey?: string; // Your API key\n traceModelRequests?: boolean; // Enable model request tracing (default: false)\n}\n\nclass OctavusClient {\n readonly agents: AgentsApi;\n readonly agentSessions: AgentSessionsApi;\n readonly workers: WorkersApi;\n readonly files: FilesApi;\n\n constructor(config: OctavusClientConfig);\n}\n```\n\n### AgentSessionsApi\n\nManages agent sessions.\n\n```typescript\nclass AgentSessionsApi {\n // Create a new session\n async create(agentId: string, input?: Record<string, unknown>): Promise<string>;\n\n // Get full session state (for debugging/internal use)\n async get(sessionId: string): Promise<SessionState>;\n\n // Get UI-ready messages (for client display)\n async getMessages(sessionId: string): Promise<UISessionState>;\n\n // Attach to a session for triggering\n attach(sessionId: string, options?: SessionAttachOptions): AgentSession;\n}\n\n// Full session state (internal format)\ninterface SessionState {\n id: string;\n agentId: string;\n input: Record<string, unknown>;\n variables: Record<string, unknown>;\n resources: Record<string, unknown>;\n messages: ChatMessage[]; // Internal message format\n createdAt: string;\n updatedAt: string;\n}\n\n// UI-ready session state\ninterface UISessionState {\n sessionId: string;\n agentId: string;\n messages: UIMessage[]; // UI-ready messages for frontend\n}\n```\n\n### AgentSession\n\nHandles request execution and streaming for a specific session.\n\n```typescript\nclass AgentSession {\n // Execute a request and stream parsed events\n execute(request: SessionRequest, options?: TriggerOptions): AsyncGenerator<StreamEvent>;\n\n // Get the session ID\n getSessionId(): string;\n\n // Register dynamic tools (e.g., pass a Computer or explicit DynamicTool[])\n setDynamicTools(source: ToolProvider | DynamicTool[]): void;\n}\n\ntype SessionRequest = TriggerRequest | ContinueRequest;\n\ninterface TriggerRequest {\n type: 'trigger';\n triggerName: string;\n input?: Record<string, unknown>;\n}\n\ninterface ContinueRequest {\n type: 'continue';\n executionId: string;\n toolResults: ToolResult[];\n}\n\n// Helper to convert events to SSE stream\nfunction toSSEStream(events: AsyncIterable<StreamEvent>): ReadableStream<Uint8Array>;\n```\n\n### FilesApi\n\nHandles file uploads for sessions.\n\n```typescript\nclass FilesApi {\n // Get presigned URLs for file uploads\n async getUploadUrls(sessionId: string, files: FileUploadRequest[]): Promise<UploadUrlsResponse>;\n}\n\ninterface FileUploadRequest {\n filename: string;\n mediaType: string;\n size: number;\n}\n\ninterface UploadUrlsResponse {\n files: {\n id: string; // File ID for references\n uploadUrl: string; // PUT to this URL\n downloadUrl: string; // GET URL after upload\n }[];\n}\n```\n\nThe client uploads files directly to S3 using the presigned upload URL. See [File Uploads](/docs/client-sdk/file-uploads) for the full integration pattern.\n\n## Next Steps\n\n- [Sessions](/docs/server-sdk/sessions) - Deep dive into session management\n- [Tools](/docs/server-sdk/tools) - Implementing tool handlers\n- [Streaming](/docs/server-sdk/streaming) - Understanding stream events\n- [Workers](/docs/server-sdk/workers) - Executing worker agents\n- [Debugging](/docs/server-sdk/debugging) - Model request tracing and debugging\n- [Computer](/docs/server-sdk/computer) - Browser, filesystem, and shell via MCP\n",
|
|
763
772
|
excerpt: "Server SDK Overview The package provides a Node.js SDK for integrating Octavus agents into your backend application. It handles session management, streaming, and the tool execution continuation...",
|
|
764
773
|
order: 1
|
|
765
774
|
},
|
|
@@ -768,7 +777,7 @@ var sections_default = [
|
|
|
768
777
|
section: "server-sdk",
|
|
769
778
|
title: "Sessions",
|
|
770
779
|
description: "Managing agent sessions with the Server SDK.",
|
|
771
|
-
content: "\n# Sessions\n\nSessions represent conversations with an agent. They store conversation history, track resources and variables, and enable stateful interactions.\n\n## Creating Sessions\n\nCreate a session by specifying the agent ID and initial input variables:\n\n```typescript\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n});\n\n// Create a session with the support-chat agent\nconst sessionId = await client.agentSessions.create('support-chat', {\n COMPANY_NAME: 'Acme Corp',\n PRODUCT_NAME: 'Widget Pro',\n USER_ID: 'user-123', // Optional inputs\n});\n\nconsole.log('Session created:', sessionId);\n```\n\n## Getting Session Messages\n\nTo restore a conversation on page load, use `getMessages()` to retrieve UI-ready messages:\n\n```typescript\nconst session = await client.agentSessions.getMessages(sessionId);\n\nconsole.log({\n sessionId: session.sessionId,\n agentId: session.agentId,\n messages: session.messages.length, // UIMessage[] ready for frontend\n});\n```\n\nThe returned messages can be passed directly to the client SDK's `initialMessages` option.\n\n### UISessionState Interface\n\n```typescript\ninterface UISessionState {\n sessionId: string;\n agentId: string;\n messages: UIMessage[]; // UI-ready conversation history\n}\n```\n\n## Full Session State (Debug)\n\nFor debugging or internal use, you can retrieve the complete session state including all variables and internal message format:\n\n```typescript\nconst state = await client.agentSessions.get(sessionId);\n\nconsole.log({\n id: state.id,\n agentId: state.agentId,\n messages: state.messages.length, // ChatMessage[] (internal format)\n resources: state.resources,\n variables: state.variables,\n createdAt: state.createdAt,\n updatedAt: state.updatedAt,\n});\n```\n\n> **Note**: Use `getMessages()` for client-facing code. The `get()` method returns internal message format that includes hidden content not intended for end users.\n\n## Attaching to Sessions\n\nTo trigger actions on a session, you need to attach to it first:\n\n```typescript\nconst session = client.agentSessions.attach(sessionId, {\n tools: {\n // Tool handlers (see Tools documentation)\n },\n resources: [\n // Resource watchers (optional)\n ],\n});\n```\n\n### Attach Options\n\n| Option | Type | Description |\n| ----------------------- | --------------------------------- | ------------------------------------------------------------------------------- |\n| `tools` | `ToolHandlers` | Server-side tool handler functions |\n| `resources` | `Resource[]` | Resource watchers for real-time updates |\n| `onToolResults` | `(results: ToolResult[]) => void` | Callback invoked after server-side tool results are produced |\n| `rejectClientToolCalls` | `boolean` | If `true`, reject tool calls that have no server handler (no client forwarding) |\n\nFor MCP tool integration (browser, filesystem, shell via `@octavus/computer`), register dynamic tools after attaching with `session.setDynamicTools()`. See [Computer](/docs/server-sdk/computer) for details.\n\n## Executing Requests\n\nOnce attached, execute requests on the session using `execute()`:\n\n```typescript\nimport { toSSEStream } from '@octavus/server-sdk';\n\n// execute() handles both triggers and client tool continuations\nconst events = session.execute(\n { type: 'trigger', triggerName: 'user-message', input: { USER_MESSAGE: 'Hello!' } },\n { signal: request.signal },\n);\n\n// Convert to SSE stream for HTTP responses\nreturn new Response(toSSEStream(events), {\n headers: { 'Content-Type': 'text/event-stream' },\n});\n```\n\n### Request Types\n\nThe `execute()` method accepts a discriminated union:\n\n```typescript\ntype SessionRequest = TriggerRequest | ContinueRequest;\n\n// Start a new conversation turn\ninterface TriggerRequest {\n type: 'trigger';\n triggerName: string;\n input?: Record<string, unknown>;\n rollbackAfterMessageId?: string | null; // For retry: truncate messages after this ID\n}\n\n// Continue after client-side tool handling\ninterface ContinueRequest {\n type: 'continue';\n executionId: string;\n toolResults: ToolResult[];\n}\n```\n\nThis makes it easy to pass requests through from the client:\n\n```typescript\n// Simple passthrough from HTTP request body\nexport async function POST(request: Request) {\n const body = await request.json();\n const { sessionId, ...payload } = body;\n\n const session = client.agentSessions.attach(sessionId, {\n tools: {\n /* ... */\n },\n });\n const events = session.execute(payload, { signal: request.signal });\n\n return new Response(toSSEStream(events));\n}\n```\n\n### Stop Support\n\nPass an abort signal to allow clients to stop generation:\n\n```typescript\nconst events = session.execute(request, {\n signal: request.signal, // Forward the client's abort signal\n});\n```\n\nWhen the client aborts the request, the signal propagates through to the LLM provider, stopping generation immediately. Any partial content is preserved.\n\n## WebSocket Handling\n\nFor WebSocket integrations, use `handleSocketMessage()` which manages abort controller lifecycle internally:\n\n```typescript\nimport type { SocketMessage } from '@octavus/server-sdk';\n\n// In your socket handler\nconn.on('data', async (rawData: string) => {\n const msg = JSON.parse(rawData);\n\n if (msg.type === 'trigger' || msg.type === 'continue' || msg.type === 'stop') {\n await session.handleSocketMessage(msg as SocketMessage, {\n onEvent: (event) => conn.write(JSON.stringify(event)),\n onFinish: async () => {\n // Fetch and persist messages to your database for restoration\n },\n });\n }\n});\n```\n\nThe `handleSocketMessage()` method:\n\n- Handles `trigger`, `continue`, and `stop` messages\n- Automatically aborts previous requests when a new one arrives\n- Streams events via the `onEvent` callback\n- Calls `onFinish` after streaming completes (not called if aborted)\n\nSee [Socket Chat Example](/docs/examples/socket-chat) for a complete implementation.\n\n## Session Lifecycle\n\n```mermaid\nflowchart TD\n A[1. CREATE] --> B[2. ATTACH]\n B --> C[3. TRIGGER]\n C --> C\n C --> D[4. RETRIEVE]\n D --> C\n C --> E[5. EXPIRE]\n C --> G[5b. CLEAR]\n G --> F\n E --> F{6. RESTORE?}\n F -->|Yes| C\n F -->|No| A\n\n A -.- A1[\"`**client.agentSessions.create()**\n Returns sessionId\n Initializes state`\"]\n\n B -.- B1[\"`**client.agentSessions.attach()**\n Configure tool handlers\n Configure resource watchers`\"]\n\n C -.- C1[\"`**session.execute()**\n Execute request\n Stream events\n Update state`\"]\n\n D -.- D1[\"`**client.agentSessions.getMessages()**\n Get UI-ready messages\n Check session status`\"]\n\n E -.- E1[\"`Sessions expire after\n 24 hours (configurable)`\"]\n\n G -.- G1[\"`**client.agentSessions.clear()**\n Programmatically clear state\n Session becomes expired`\"]\n\n F -.- F1[\"`**client.agentSessions.restore()**\n Restore from stored messages\n Or create new session`\"]\n```\n\n## Session Expiration\n\nSessions expire after a period of inactivity (default: 24 hours). When you call `getMessages()` or `get()`, the response includes a `status` field:\n\n```typescript\nconst result = await client.agentSessions.getMessages(sessionId);\n\nif (result.status === 'expired') {\n // Session has expired - restore or create new\n console.log('Session expired:', result.sessionId);\n} else {\n // Session is active\n console.log('Messages:', result.messages.length);\n}\n```\n\n### Response Types\n\n| Status | Type | Description |\n| --------- | --------------------- | ------------------------------------------------------------- |\n| `active` | `UISessionState` | Session is active, includes `messages` array |\n| `expired` | `ExpiredSessionState` | Session expired, includes `sessionId`, `agentId`, `createdAt` |\n\n## Persisting Chat History\n\nTo enable session restoration, store the chat messages in your own database after each interaction:\n\n```typescript\n// After each trigger completes, save messages\nconst result = await client.agentSessions.getMessages(sessionId);\n\nif (result.status === 'active') {\n // Store in your database\n await db.chats.update({\n where: { id: chatId },\n data: {\n sessionId: result.sessionId,\n messages: result.messages, // Store UIMessage[] as JSON\n },\n });\n}\n```\n\n> **Best Practice**: Store the full `UIMessage[]` array. This preserves all message parts (text, tool calls, files, etc.) needed for accurate restoration.\n\n## Restoring Sessions\n\nWhen a user returns to your app:\n\n```typescript\n// 1. Load stored data from your database\nconst chat = await db.chats.findUnique({ where: { id: chatId } });\n\n// 2. Check if session is still active\nconst result = await client.agentSessions.getMessages(chat.sessionId);\n\nif (result.status === 'active') {\n // Session is active - use it directly\n return {\n sessionId: result.sessionId,\n messages: result.messages,\n };\n}\n\n// 3. Session expired - restore from stored messages\nif (chat.messages && chat.messages.length > 0) {\n const restored = await client.agentSessions.restore(\n chat.sessionId,\n chat.messages,\n { COMPANY_NAME: 'Acme Corp' }, // Optional: same input as create()\n );\n\n if (restored.restored) {\n // Session restored successfully\n return {\n sessionId: restored.sessionId,\n messages: chat.messages,\n };\n }\n}\n\n// 4. Cannot restore - create new session\nconst newSessionId = await client.agentSessions.create('support-chat', {\n COMPANY_NAME: 'Acme Corp',\n});\n\nreturn {\n sessionId: newSessionId,\n messages: [],\n};\n```\n\n### Restore Response\n\n```typescript\ninterface RestoreSessionResult {\n sessionId: string;\n restored: boolean; // true if restored, false if session was already active\n}\n```\n\n## Complete Example\n\nHere's a complete session management flow:\n\n```typescript\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n});\n\nasync function getOrCreateSession(chatId: string, agentId: string, input: Record<string, unknown>) {\n // Load existing chat data\n const chat = await db.chats.findUnique({ where: { id: chatId } });\n\n if (chat?.sessionId) {\n // Check session status\n const result = await client.agentSessions.getMessages(chat.sessionId);\n\n if (result.status === 'active') {\n return { sessionId: result.sessionId, messages: result.messages };\n }\n\n // Try to restore expired session\n if (chat.messages?.length > 0) {\n const restored = await client.agentSessions.restore(chat.sessionId, chat.messages, input);\n if (restored.restored) {\n return { sessionId: restored.sessionId, messages: chat.messages };\n }\n }\n }\n\n // Create new session\n const sessionId = await client.agentSessions.create(agentId, input);\n\n // Save to database\n await db.chats.upsert({\n where: { id: chatId },\n create: { id: chatId, sessionId, messages: [] },\n update: { sessionId, messages: [] },\n });\n\n return { sessionId, messages: [] };\n}\n```\n\n## Clearing Sessions\n\nTo programmatically clear a session's state (e.g., for testing reset/restore flows), use `clear()`:\n\n```typescript\nconst result = await client.agentSessions.clear(sessionId);\nconsole.log(result.cleared); // true\n```\n\nAfter clearing, the session transitions to `expired` status. You can then restore it with `restore()` or create a new session.\n\n```typescript\ninterface ClearSessionResult {\n sessionId: string;\n cleared: boolean;\n}\n```\n\nThis is idempotent - calling `clear()` on an already expired session succeeds without error.\n\n## Error Handling\n\n```typescript\nimport { ApiError } from '@octavus/server-sdk';\n\ntry {\n const session = await client.agentSessions.getMessages(sessionId);\n} catch (error) {\n if (error instanceof ApiError) {\n if (error.status === 404) {\n // Session not found or expired\n console.log('Session expired, create a new one');\n } else {\n console.error('API Error:', error.message);\n }\n }\n throw error;\n}\n```\n",
|
|
780
|
+
content: "\n# Sessions\n\nSessions represent conversations with an agent. They store conversation history, track resources and variables, and enable stateful interactions.\n\n## Creating Sessions\n\nCreate a session by specifying the agent ID and initial input variables:\n\n```typescript\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n});\n\n// Create a session with the support-chat agent\nconst sessionId = await client.agentSessions.create('support-chat', {\n COMPANY_NAME: 'Acme Corp',\n PRODUCT_NAME: 'Widget Pro',\n USER_ID: 'user-123', // Optional inputs\n});\n\nconsole.log('Session created:', sessionId);\n```\n\n## Getting Session Messages\n\nTo restore a conversation on page load, use `getMessages()` to retrieve UI-ready messages:\n\n```typescript\nconst session = await client.agentSessions.getMessages(sessionId);\n\nconsole.log({\n sessionId: session.sessionId,\n agentId: session.agentId,\n messages: session.messages.length, // UIMessage[] ready for frontend\n});\n```\n\nThe returned messages can be passed directly to the client SDK's `initialMessages` option.\n\n### UISessionState Interface\n\n```typescript\ninterface UISessionState {\n sessionId: string;\n agentId: string;\n messages: UIMessage[]; // UI-ready conversation history\n}\n```\n\n## Full Session State (Debug)\n\nFor debugging or internal use, you can retrieve the complete session state including all variables and internal message format:\n\n```typescript\nconst state = await client.agentSessions.get(sessionId);\n\nconsole.log({\n id: state.id,\n agentId: state.agentId,\n messages: state.messages.length, // ChatMessage[] (internal format)\n resources: state.resources,\n variables: state.variables,\n createdAt: state.createdAt,\n updatedAt: state.updatedAt,\n});\n```\n\n> **Note**: Use `getMessages()` for client-facing code. The `get()` method returns internal message format that includes hidden content not intended for end users.\n\n## Getting Execution Logs\n\n`getLogs()` returns the chronological execution trace for a session - triggers, messages, tool calls, LLM responses, errors, and other events emitted while the agent ran. Useful for debugging, observability, and building custom timeline views.\n\n```typescript\nconst result = await client.agentSessions.getLogs(sessionId);\n\nif (result.status === 'expired') {\n console.log('Session expired:', result.sessionId);\n} else {\n for (const entry of result.entries) {\n console.log(entry.type, entry.timestamp);\n }\n}\n```\n\nEach entry is a typed variant of `ExecutionLogEntry` (a discriminated union) so consumers can narrow on `entry.type`:\n\n```typescript\nconst result = await client.agentSessions.getLogs(sessionId);\n\nif (result.status !== 'expired') {\n const toolCalls = result.entries.filter((e) => e.type === 'tool-call');\n for (const call of toolCalls) {\n // call.toolName, call.toolArguments are typed without optional chaining\n console.log(call.toolName, call.toolArguments);\n }\n}\n```\n\n### Excluding Model Request Payloads\n\nModel-request entries include the full provider request body and can be large. Pass `excludeModelRequests: true` to skip them:\n\n```typescript\nconst result = await client.agentSessions.getLogs(sessionId, {\n excludeModelRequests: true,\n});\n```\n\n### Truncation\n\nResponses are capped at 1000 entries (most recent). When the log exceeds that cap, the response includes `total` and `truncated` so consumers can detect this:\n\n```typescript\nconst result = await client.agentSessions.getLogs(sessionId);\n\nif (result.status !== 'expired' && result.truncated) {\n console.warn(`Showing latest 1000 of ${result.total} entries`);\n}\n```\n\n### Response Types\n\n| Status | Type | Description |\n| --------- | --------------------- | -------------------------------------------------------------------------------------------- |\n| `active` | `ExecutionLogsResult` | `{ sessionId, entries, total?, truncated? }`. `total` and `truncated` are present when known |\n| `expired` | `ExpiredSessionState` | `{ sessionId, agentId, status: 'expired', createdAt }` |\n\n> **Forward-compatible types**: `ExecutionLogEntry` may gain new variants over time. Include a `default` case when switching on `entry.type` so unknown variants are handled gracefully.\n\n## Attaching to Sessions\n\nTo trigger actions on a session, you need to attach to it first:\n\n```typescript\nconst session = client.agentSessions.attach(sessionId, {\n tools: {\n // Tool handlers (see Tools documentation)\n },\n resources: [\n // Resource watchers (optional)\n ],\n});\n```\n\n### Attach Options\n\n| Option | Type | Description |\n| ----------------------- | --------------------------------- | ------------------------------------------------------------------------------- |\n| `tools` | `ToolHandlers` | Server-side tool handler functions |\n| `resources` | `Resource[]` | Resource watchers for real-time updates |\n| `onToolResults` | `(results: ToolResult[]) => void` | Callback invoked after server-side tool results are produced |\n| `rejectClientToolCalls` | `boolean` | If `true`, reject tool calls that have no server handler (no client forwarding) |\n\nFor MCP tool integration (browser, filesystem, shell via `@octavus/computer`), register dynamic tools after attaching with `session.setDynamicTools()`. See [Computer](/docs/server-sdk/computer) for details.\n\n## Executing Requests\n\nOnce attached, execute requests on the session using `execute()`:\n\n```typescript\nimport { toSSEStream } from '@octavus/server-sdk';\n\n// execute() handles both triggers and client tool continuations\nconst events = session.execute(\n { type: 'trigger', triggerName: 'user-message', input: { USER_MESSAGE: 'Hello!' } },\n { signal: request.signal },\n);\n\n// Convert to SSE stream for HTTP responses\nreturn new Response(toSSEStream(events), {\n headers: { 'Content-Type': 'text/event-stream' },\n});\n```\n\n### Request Types\n\nThe `execute()` method accepts a discriminated union:\n\n```typescript\ntype SessionRequest = TriggerRequest | ContinueRequest;\n\n// Start a new conversation turn\ninterface TriggerRequest {\n type: 'trigger';\n triggerName: string;\n input?: Record<string, unknown>;\n rollbackAfterMessageId?: string | null; // For retry: truncate messages after this ID\n}\n\n// Continue after client-side tool handling\ninterface ContinueRequest {\n type: 'continue';\n executionId: string;\n toolResults: ToolResult[];\n}\n```\n\nThis makes it easy to pass requests through from the client:\n\n```typescript\n// Simple passthrough from HTTP request body\nexport async function POST(request: Request) {\n const body = await request.json();\n const { sessionId, ...payload } = body;\n\n const session = client.agentSessions.attach(sessionId, {\n tools: {\n /* ... */\n },\n });\n const events = session.execute(payload, { signal: request.signal });\n\n return new Response(toSSEStream(events));\n}\n```\n\n### Stop Support\n\nPass an abort signal to allow clients to stop generation:\n\n```typescript\nconst events = session.execute(request, {\n signal: request.signal, // Forward the client's abort signal\n});\n```\n\nWhen the client aborts the request, the signal propagates through to the LLM provider, stopping generation immediately. Any partial content is preserved.\n\n## WebSocket Handling\n\nFor WebSocket integrations, use `handleSocketMessage()` which manages abort controller lifecycle internally:\n\n```typescript\nimport type { SocketMessage } from '@octavus/server-sdk';\n\n// In your socket handler\nconn.on('data', async (rawData: string) => {\n const msg = JSON.parse(rawData);\n\n if (msg.type === 'trigger' || msg.type === 'continue' || msg.type === 'stop') {\n await session.handleSocketMessage(msg as SocketMessage, {\n onEvent: (event) => conn.write(JSON.stringify(event)),\n onFinish: async () => {\n // Fetch and persist messages to your database for restoration\n },\n });\n }\n});\n```\n\nThe `handleSocketMessage()` method:\n\n- Handles `trigger`, `continue`, and `stop` messages\n- Automatically aborts previous requests when a new one arrives\n- Streams events via the `onEvent` callback\n- Calls `onFinish` after streaming completes (not called if aborted)\n\nSee [Socket Chat Example](/docs/examples/socket-chat) for a complete implementation.\n\n## Session Lifecycle\n\n```mermaid\nflowchart TD\n A[1. CREATE] --> B[2. ATTACH]\n B --> C[3. TRIGGER]\n C --> C\n C --> D[4. RETRIEVE]\n D --> C\n C --> E[5. EXPIRE]\n C --> G[5b. CLEAR]\n G --> F\n E --> F{6. RESTORE?}\n F -->|Yes| C\n F -->|No| A\n\n A -.- A1[\"`**client.agentSessions.create()**\n Returns sessionId\n Initializes state`\"]\n\n B -.- B1[\"`**client.agentSessions.attach()**\n Configure tool handlers\n Configure resource watchers`\"]\n\n C -.- C1[\"`**session.execute()**\n Execute request\n Stream events\n Update state`\"]\n\n D -.- D1[\"`**client.agentSessions.getMessages()**\n Get UI-ready messages\n Check session status`\"]\n\n E -.- E1[\"`Sessions expire after\n 24 hours (configurable)`\"]\n\n G -.- G1[\"`**client.agentSessions.clear()**\n Programmatically clear state\n Session becomes expired`\"]\n\n F -.- F1[\"`**client.agentSessions.restore()**\n Restore from stored messages\n Or create new session`\"]\n```\n\n## Session Expiration\n\nSessions expire after a period of inactivity (default: 24 hours). When you call `getMessages()` or `get()`, the response includes a `status` field:\n\n```typescript\nconst result = await client.agentSessions.getMessages(sessionId);\n\nif (result.status === 'expired') {\n // Session has expired - restore or create new\n console.log('Session expired:', result.sessionId);\n} else {\n // Session is active\n console.log('Messages:', result.messages.length);\n}\n```\n\n### Response Types\n\n| Status | Type | Description |\n| --------- | --------------------- | ------------------------------------------------------------- |\n| `active` | `UISessionState` | Session is active, includes `messages` array |\n| `expired` | `ExpiredSessionState` | Session expired, includes `sessionId`, `agentId`, `createdAt` |\n\n## Persisting Chat History\n\nTo enable session restoration, store the chat messages in your own database after each interaction:\n\n```typescript\n// After each trigger completes, save messages\nconst result = await client.agentSessions.getMessages(sessionId);\n\nif (result.status === 'active') {\n // Store in your database\n await db.chats.update({\n where: { id: chatId },\n data: {\n sessionId: result.sessionId,\n messages: result.messages, // Store UIMessage[] as JSON\n },\n });\n}\n```\n\n> **Best Practice**: Store the full `UIMessage[]` array. This preserves all message parts (text, tool calls, files, etc.) needed for accurate restoration.\n\n## Restoring Sessions\n\nWhen a user returns to your app:\n\n```typescript\n// 1. Load stored data from your database\nconst chat = await db.chats.findUnique({ where: { id: chatId } });\n\n// 2. Check if session is still active\nconst result = await client.agentSessions.getMessages(chat.sessionId);\n\nif (result.status === 'active') {\n // Session is active - use it directly\n return {\n sessionId: result.sessionId,\n messages: result.messages,\n };\n}\n\n// 3. Session expired - restore from stored messages\nif (chat.messages && chat.messages.length > 0) {\n const restored = await client.agentSessions.restore(\n chat.sessionId,\n chat.messages,\n { COMPANY_NAME: 'Acme Corp' }, // Optional: same input as create()\n );\n\n if (restored.restored) {\n // Session restored successfully\n return {\n sessionId: restored.sessionId,\n messages: chat.messages,\n };\n }\n}\n\n// 4. Cannot restore - create new session\nconst newSessionId = await client.agentSessions.create('support-chat', {\n COMPANY_NAME: 'Acme Corp',\n});\n\nreturn {\n sessionId: newSessionId,\n messages: [],\n};\n```\n\n### Restore Response\n\n```typescript\ninterface RestoreSessionResult {\n sessionId: string;\n restored: boolean; // true if restored, false if session was already active\n}\n```\n\n## Complete Example\n\nHere's a complete session management flow:\n\n```typescript\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst client = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n});\n\nasync function getOrCreateSession(chatId: string, agentId: string, input: Record<string, unknown>) {\n // Load existing chat data\n const chat = await db.chats.findUnique({ where: { id: chatId } });\n\n if (chat?.sessionId) {\n // Check session status\n const result = await client.agentSessions.getMessages(chat.sessionId);\n\n if (result.status === 'active') {\n return { sessionId: result.sessionId, messages: result.messages };\n }\n\n // Try to restore expired session\n if (chat.messages?.length > 0) {\n const restored = await client.agentSessions.restore(chat.sessionId, chat.messages, input);\n if (restored.restored) {\n return { sessionId: restored.sessionId, messages: chat.messages };\n }\n }\n }\n\n // Create new session\n const sessionId = await client.agentSessions.create(agentId, input);\n\n // Save to database\n await db.chats.upsert({\n where: { id: chatId },\n create: { id: chatId, sessionId, messages: [] },\n update: { sessionId, messages: [] },\n });\n\n return { sessionId, messages: [] };\n}\n```\n\n## Clearing Sessions\n\nTo programmatically clear a session's state (e.g., for testing reset/restore flows), use `clear()`:\n\n```typescript\nconst result = await client.agentSessions.clear(sessionId);\nconsole.log(result.cleared); // true\n```\n\nAfter clearing, the session transitions to `expired` status. You can then restore it with `restore()` or create a new session.\n\n```typescript\ninterface ClearSessionResult {\n sessionId: string;\n cleared: boolean;\n}\n```\n\nThis is idempotent - calling `clear()` on an already expired session succeeds without error.\n\n## Error Handling\n\n```typescript\nimport { ApiError } from '@octavus/server-sdk';\n\ntry {\n const session = await client.agentSessions.getMessages(sessionId);\n} catch (error) {\n if (error instanceof ApiError) {\n if (error.status === 404) {\n // Session not found or expired\n console.log('Session expired, create a new one');\n } else {\n console.error('API Error:', error.message);\n }\n }\n throw error;\n}\n```\n",
|
|
772
781
|
excerpt: "Sessions Sessions represent conversations with an agent. They store conversation history, track resources and variables, and enable stateful interactions. Creating Sessions Create a session by...",
|
|
773
782
|
order: 2
|
|
774
783
|
},
|
|
@@ -777,8 +786,8 @@ var sections_default = [
|
|
|
777
786
|
section: "server-sdk",
|
|
778
787
|
title: "Tools",
|
|
779
788
|
description: "Implementing tool handlers with the Server SDK.",
|
|
780
|
-
content: "\n# Tools\n\nTools extend what agents can do. In Octavus, tools can execute either on your server or on the client side.\n\n## Server Tools vs Client Tools\n\n| Location
|
|
781
|
-
excerpt: "Tools Tools extend what agents can do. In Octavus, tools can execute either on your server or on the client side. Server Tools vs Client Tools | Location
|
|
789
|
+
content: "\n# Tools\n\nTools extend what agents can do. In Octavus, tools can execute either on your server or on the client side.\n\n## Server Tools vs Client Tools\n\n| Location | Use Case | Registration |\n| -------------- | ------------------------------------------------- | -------------------------------------------------------- |\n| **Server** | Database queries, API calls, sensitive operations | Register handler in `attach()` |\n| **Inline MCP** | Group an integration's tools (GitHub, Salesforce) | [`createInlineMcpServer()`](/docs/server-sdk/inline-mcp) |\n| **Computer** | Browser, filesystem, shell, external services | [`session.setDynamicTools()`](/docs/server-sdk/computer) |\n| **Client** | Browser APIs, interactive UIs, confirmations | No server handler (forwarded to client) |\n\nWhen the Server SDK encounters a tool call:\n\n1. **Handler exists** (server, inline MCP, or dynamic) \u2192 Execute on server, continue automatically\n2. **No handler** \u2192 Forward to client via `client-tool-request` event\n\nInline MCP tools and dynamic tools registered via `session.setDynamicTools()` (e.g., from `@octavus/computer`) work identically to manual handlers from the platform's perspective. See [Inline MCP Servers](/docs/server-sdk/inline-mcp) for namespaced consumer-defined tool groups, and [Computer](/docs/server-sdk/computer) for device-side MCPs.\n\nFor client-side tool handling, see [Client Tools](/docs/client-sdk/client-tools).\n\n## Why Server Tools\n\nServer-side tools give you full control:\n\n- \u2705 **Full data access** - Query your database directly\n- \u2705 **Your authentication** - Use your existing auth context\n- \u2705 **No data exposure** - Sensitive data never leaves your infrastructure\n- \u2705 **Custom logic** - Any complexity you need\n\n## Defining Tool Handlers\n\nTool handlers are async functions that receive arguments and return results:\n\n```typescript\nimport type { ToolHandlers } from '@octavus/server-sdk';\n\nconst tools: ToolHandlers = {\n 'get-user-account': async (args) => {\n const userId = args.userId as string;\n\n // Query your database\n const user = await db.users.findById(userId);\n\n return {\n name: user.name,\n email: user.email,\n plan: user.subscription.plan,\n createdAt: user.createdAt.toISOString(),\n };\n },\n\n 'create-support-ticket': async (args) => {\n const summary = args.summary as string;\n const priority = args.priority as string;\n\n // Create ticket in your system\n const ticket = await ticketService.create({\n summary,\n priority,\n source: 'ai-chat',\n });\n\n return {\n ticketId: ticket.id,\n estimatedResponse: getEstimatedResponse(priority),\n };\n },\n};\n```\n\n## Using Tools in Sessions\n\nPass tool handlers when attaching to a session:\n\n```typescript\nconst session = client.agentSessions.attach(sessionId, {\n tools: {\n 'get-user-account': async (args) => {\n // Implementation\n },\n 'create-support-ticket': async (args) => {\n // Implementation\n },\n },\n});\n```\n\n## Tool Handler Signature\n\n```typescript\ntype ToolHandler = (args: Record<string, unknown>) => Promise<unknown>;\ntype ToolHandlers = Record<string, ToolHandler>;\n```\n\n### Arguments\n\nArguments are passed as a `Record<string, unknown>`. Type-check as needed:\n\n```typescript\n'search-products': async (args) => {\n const query = args.query as string;\n const category = args.category as string | undefined;\n const maxPrice = args.maxPrice as number | undefined;\n\n return await productSearch({ query, category, maxPrice });\n}\n```\n\n### Return Values\n\nReturn any JSON-serializable value. The result is:\n\n1. Sent back to the LLM as context\n2. Stored in session state\n3. Optionally stored in a variable for protocol use\n\n```typescript\n// Return object\nreturn { id: '123', status: 'created' };\n\n// Return array\nreturn [{ id: '1' }, { id: '2' }];\n\n// Return primitive\nreturn 42;\n\n// Return null for \"no result\"\nreturn null;\n```\n\n## Error Handling\n\nThrow errors for failures. They're captured and sent to the LLM:\n\n```typescript\n'get-user-account': async (args) => {\n const userId = args.userId as string;\n\n const user = await db.users.findById(userId);\n\n if (!user) {\n throw new Error(`User not found: ${userId}`);\n }\n\n return user;\n}\n```\n\nThe LLM receives the error message and can respond appropriately (e.g., \"I couldn't find that account\").\n\n## Tool Execution Flow\n\nWhen the LLM calls a tool:\n\n```mermaid\nsequenceDiagram\n participant LLM\n participant Platform as Octavus Platform\n participant SDK as Server SDK\n participant Client as Client SDK\n\n LLM->>Platform: 1. Decides to call tool\n Platform-->>Client: tool-input-start, tool-input-delta\n Platform-->>Client: tool-input-available\n Platform-->>SDK: 2. tool-request (stream pauses)\n\n alt Has server handler\n Note over SDK: 3a. Execute handler<br/>tools['get-user']()\n SDK-->>Client: tool-output-available\n SDK->>Platform: Continue with results\n else No server handler\n SDK-->>Client: 3b. client-tool-request\n Note over Client: Execute client tool<br/>or show interactive UI\n Client->>SDK: Tool results\n SDK->>Platform: Continue with results\n end\n\n Platform->>LLM: 4. Process results\n LLM-->>Platform: Response\n Platform-->>Client: text-delta events\n\n Note over LLM,Client: 5. Repeat if more tools needed\n```\n\n## Accessing Request Context\n\nFor request-specific data (auth, headers), create handlers dynamically:\n\n```typescript\nimport { toSSEStream } from '@octavus/server-sdk';\n\n// In your API route\nexport async function POST(request: Request) {\n const body = await request.json();\n const { sessionId, ...payload } = body;\n\n const authToken = request.headers.get('Authorization');\n const user = await validateToken(authToken);\n\n const session = client.agentSessions.attach(sessionId, {\n tools: {\n 'get-user-account': async (args) => {\n // Use request context\n return await db.users.findById(user.id);\n },\n 'create-order': async (args) => {\n // Create with user context\n return await orderService.create({\n ...args,\n userId: user.id,\n createdBy: user.email,\n });\n },\n // Tools without handlers here are forwarded to the client\n },\n });\n\n const events = session.execute(payload, { signal: request.signal });\n return new Response(toSSEStream(events));\n}\n```\n\n## Best Practices\n\n### 1. Validate Arguments\n\n```typescript\n'create-ticket': async (args) => {\n const summary = args.summary;\n if (typeof summary !== 'string' || summary.length === 0) {\n throw new Error('Summary is required');\n }\n // ...\n}\n```\n\n### 2. Handle Timeouts\n\n```typescript\n'external-api-call': async (args) => {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 10000);\n\n try {\n const response = await fetch(url, { signal: controller.signal });\n return await response.json();\n } finally {\n clearTimeout(timeout);\n }\n}\n```\n\n### 3. Log Tool Calls\n\n```typescript\n'search-products': async (args) => {\n console.log('Tool call: search-products', { args });\n\n const result = await productSearch(args);\n\n console.log('Tool result: search-products', {\n resultCount: result.length\n });\n\n return result;\n}\n```\n",
|
|
790
|
+
excerpt: "Tools Tools extend what agents can do. In Octavus, tools can execute either on your server or on the client side. Server Tools vs Client Tools | Location | Use Case ...",
|
|
782
791
|
order: 3
|
|
783
792
|
},
|
|
784
793
|
{
|
|
@@ -795,7 +804,7 @@ var sections_default = [
|
|
|
795
804
|
section: "server-sdk",
|
|
796
805
|
title: "CLI",
|
|
797
806
|
description: "Command-line interface for validating and syncing agent definitions.",
|
|
798
|
-
content: '\n# Octavus CLI\n\nThe `@octavus/cli` package provides a command-line interface for validating and syncing agent definitions from your local filesystem to the Octavus platform.\n\n**Current version:** `3.
|
|
807
|
+
content: '\n# Octavus CLI\n\nThe `@octavus/cli` package provides a command-line interface for validating and syncing agent definitions from your local filesystem to the Octavus platform.\n\n**Current version:** `3.5.0`\n\n## Installation\n\n```bash\nnpm install --save-dev @octavus/cli\n```\n\n## Configuration\n\nThe CLI requires an API key with the **Agents** permission.\n\n### Environment Variables\n\n| Variable | Description |\n| --------------------- | ---------------------------------------------- |\n| `OCTAVUS_CLI_API_KEY` | API key with "Agents" permission (recommended) |\n| `OCTAVUS_API_KEY` | Fallback if `OCTAVUS_CLI_API_KEY` not set |\n| `OCTAVUS_API_URL` | Optional, defaults to `https://octavus.ai` |\n\n### Two-Key Strategy (Recommended)\n\nFor production deployments, use separate API keys with minimal permissions:\n\n```bash\n# CI/CD or .env.local (not committed)\nOCTAVUS_CLI_API_KEY=oct_sk_... # "Agents" permission only\n\n# Production .env\nOCTAVUS_API_KEY=oct_sk_... # "Sessions" permission only\n```\n\nThis ensures production servers only have session permissions (smaller blast radius if leaked), while agent management is restricted to development/CI environments.\n\n### Multiple Environments\n\nUse separate Octavus projects for staging and production, each with their own API keys. The `--env` flag lets you load different environment files:\n\n```bash\n# Local development (default: .env)\noctavus sync ./agents/my-agent\n\n# Staging project\noctavus --env .env.staging sync ./agents/my-agent\n\n# Production project\noctavus --env .env.production sync ./agents/my-agent\n```\n\nExample environment files:\n\n```bash\n# .env.staging (syncs to your staging project)\nOCTAVUS_CLI_API_KEY=oct_sk_staging_project_key...\n\n# .env.production (syncs to your production project)\nOCTAVUS_CLI_API_KEY=oct_sk_production_project_key...\n```\n\nEach project has its own agents, so you\'ll get different agent IDs per environment.\n\n## Global Options\n\n| Option | Description |\n| -------------- | ------------------------------------------------------- |\n| `--env <file>` | Load environment from a specific file (default: `.env`) |\n| `--help` | Show help |\n| `--version` | Show version |\n\n## Commands\n\n### `octavus sync <path>`\n\nSync an agent definition to the platform. Creates the agent if it doesn\'t exist, or updates it if it does.\n\n```bash\noctavus sync ./agents/my-agent\n```\n\n**Options:**\n\n- `--json` - Output as JSON (for CI/CD parsing)\n- `--quiet` - Suppress non-essential output\n\n**Example output:**\n\n```\n\u2139 Reading agent from ./agents/my-agent...\n\u2139 Syncing support-chat...\n\u2713 Created: support-chat\n Agent ID: clxyz123abc456\n```\n\n### `octavus validate <path>`\n\nValidate an agent definition without saving. Useful for CI/CD pipelines.\n\n```bash\noctavus validate ./agents/my-agent\n```\n\n**Exit codes:**\n\n- `0` - Validation passed\n- `1` - Validation errors\n- `2` - Configuration errors (missing API key, etc.)\n\n### `octavus list`\n\nList all agents in your project.\n\n```bash\noctavus list\n```\n\n**Example output:**\n\n```\nSLUG NAME FORMAT ID\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nsupport-chat Support Chat Agent interactive clxyz123abc456\n\n1 agent(s)\n```\n\n### `octavus get <slug>`\n\nGet details about a specific agent by its slug.\n\n```bash\noctavus get support-chat\n```\n\n### `octavus archive <slug>`\n\nArchive an agent by slug (soft delete). Archived agents are removed from the active agent list and their slug is freed for reuse.\n\n```bash\noctavus archive support-chat\n```\n\n**Options:**\n\n- `--json` - Output as JSON (for CI/CD parsing)\n- `--quiet` - Suppress non-essential output\n\n**Example output:**\n\n```\n\u2139 Archiving support-chat...\n\u2713 Archived: support-chat\n Agent ID: clxyz123abc456\n```\n\n### `octavus skills sync <path>`\n\nSync a skill to the platform. Packages the skill directory into a bundle (excluding `.env` files, `.git`, and `node_modules`), uploads it, and optionally pushes secrets from the skill\'s `.env` file.\n\n```bash\noctavus skills sync ./skills/github\n```\n\n**Options:**\n\n- `--json` - Output as JSON (for CI/CD parsing)\n- `--quiet` - Suppress non-essential output\n\n**Example output:**\n\n```\n\u2139 Reading skill from ./skills/github...\n\u2139 Packaging github...\n\u2713 Created: github\n Skill ID: clxyz789def012\n\u2139 Pushing 2 secret(s)...\n\u2713 2 secret(s) updated\n```\n\n**Secret handling:**\n\nIf the skill directory contains a `.env` file, secrets are pushed alongside the bundle. Secrets are cross-validated against the `secrets` declarations in `SKILL.md` - warnings are shown for undeclared or missing required secrets.\n\n```\nmy-skill/\n\u251C\u2500\u2500 SKILL.md\n\u251C\u2500\u2500 scripts/\n\u2502 \u2514\u2500\u2500 run.py\n\u2514\u2500\u2500 .env # Secrets (not included in bundle)\n```\n\nSee [Skills](/docs/protocol/skills) for details on skill format, secrets, and secure mode.\n\n## Agent Directory Structure\n\nThe CLI expects agent definitions in a specific directory structure:\n\n```\nmy-agent/\n\u251C\u2500\u2500 settings.json # Required: Agent metadata\n\u251C\u2500\u2500 protocol.yaml # Required: Agent protocol\n\u251C\u2500\u2500 prompts/ # Optional: Prompt templates\n\u2502 \u251C\u2500\u2500 system.md\n\u2502 \u2514\u2500\u2500 user-message.md\n\u2514\u2500\u2500 references/ # Optional: Reference documents\n \u2514\u2500\u2500 api-guidelines.md\n```\n\n### references/\n\nReference files are markdown documents with YAML frontmatter containing a `description`. The agent can fetch these on demand during execution. See [References](/docs/protocol/references) for details.\n\n### settings.json\n\n```json\n{\n "slug": "my-agent",\n "name": "My Agent",\n "description": "A helpful assistant",\n "format": "interactive"\n}\n```\n\n### protocol.yaml\n\nSee the [Protocol documentation](/docs/protocol/overview) for details on protocol syntax.\n\n## CI/CD Integration\n\n### GitHub Actions\n\n```yaml\nname: Validate and Sync Agents\n\non:\n push:\n branches: [main]\n paths:\n - \'agents/**\'\n\njobs:\n sync:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-node@v4\n with:\n node-version: \'22\'\n\n - run: npm install\n\n - name: Validate agent\n run: npx octavus validate ./agents/support-chat\n env:\n OCTAVUS_CLI_API_KEY: ${{ secrets.OCTAVUS_CLI_API_KEY }}\n\n - name: Sync agent\n run: npx octavus sync ./agents/support-chat\n env:\n OCTAVUS_CLI_API_KEY: ${{ secrets.OCTAVUS_CLI_API_KEY }}\n```\n\n### Package.json Scripts\n\nAdd sync scripts to your `package.json`:\n\n```json\n{\n "scripts": {\n "agents:validate": "octavus validate ./agents/my-agent",\n "agents:sync": "octavus sync ./agents/my-agent"\n },\n "devDependencies": {\n "@octavus/cli": "^0.1.0"\n }\n}\n```\n\n## Workflow\n\nThe recommended workflow for managing agents:\n\n1. **Define agent locally** - Create `settings.json`, `protocol.yaml`, and prompts\n2. **Validate** - Run `octavus validate ./my-agent` to check for errors\n3. **Sync** - Run `octavus sync ./my-agent` to push to platform\n4. **Store agent ID** - Save the output ID in an environment variable\n5. **Use in app** - Read the ID from env and pass to `client.agentSessions.create()`\n\n```bash\n# After syncing: octavus sync ./agents/support-chat\n# Output: Agent ID: clxyz123abc456\n\n# Add to your .env file\nOCTAVUS_SUPPORT_AGENT_ID=clxyz123abc456\n```\n\n```typescript\nconst agentId = process.env.OCTAVUS_SUPPORT_AGENT_ID;\n\nconst sessionId = await client.agentSessions.create(agentId, {\n COMPANY_NAME: \'Acme Corp\',\n});\n```\n',
|
|
799
808
|
excerpt: "Octavus CLI The package provides a command-line interface for validating and syncing agent definitions from your local filesystem to the Octavus platform. Current version: Installation ...",
|
|
800
809
|
order: 5
|
|
801
810
|
},
|
|
@@ -822,9 +831,18 @@ var sections_default = [
|
|
|
822
831
|
section: "server-sdk",
|
|
823
832
|
title: "Computer",
|
|
824
833
|
description: "Adding browser, filesystem, and shell capabilities to agents with @octavus/computer.",
|
|
825
|
-
content: "\n# Computer\n\nThe `@octavus/computer` package gives agents access to a physical or virtual machine's browser, filesystem, and shell. It connects to [MCP](https://modelcontextprotocol.io) servers, discovers their tools, and provides them to the server-sdk.\n\n**Current version:** `3.3.0`\n\n## Installation\n\n```bash\nnpm install @octavus/computer\n```\n\n## Quick Start\n\n```typescript\nimport { Computer } from '@octavus/computer';\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', ['--browser-url=http://127.0.0.1:9222']),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', ['/path/to/workspace']),\n shell: Computer.shell({ cwd: '/path/to/workspace', mode: 'unrestricted' }),\n },\n});\n\nawait computer.start();\n\nconst client = new OctavusClient({\n baseUrl: 'https://octavus.ai',\n apiKey: 'your-api-key',\n});\n\nconst session = client.agentSessions.attach(sessionId, {\n tools: {\n 'set-chat-title': async (args) => ({ title: args.title }),\n },\n});\n\nsession.setDynamicTools(computer);\n```\n\nDynamic tools are registered after attaching via `session.setDynamicTools()`. Pass the `computer` directly - the session extracts schemas and handlers from the `ToolProvider`. Tool schemas are sent to the platform on the next `execute()` call, and tool calls flow back through the existing execution loop.\n\n## How It Works\n\n1. You configure MCP servers with namespaces (e.g., `browser`, `filesystem`, `shell`)\n2. `computer.start()` connects to all servers in parallel and discovers their tools\n3. Each tool is namespaced with `__` (e.g., `browser__navigate_page`, `filesystem__read_file`)\n4. The server-sdk sends tool schemas to the platform and handles tool call execution\n\nThe agent's protocol must declare matching `mcpServers` with `source: device` - see [MCP Servers](/docs/protocol/mcp-servers).\n\n## Entry Types\n\nThe `Computer` class supports three types of MCP entries:\n\n### Stdio (MCP Subprocess)\n\nSpawns an MCP server as a child process, communicating via stdin/stdout:\n\n```typescript\nComputer.stdio(command: string, args?: string[], options?: {\n env?: Record<string, string>;\n cwd?: string;\n})\n```\n\nUse this for local MCP servers installed as npm packages or standalone executables:\n\n```typescript\nconst computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', [\n '--browser-url=http://127.0.0.1:9222',\n '--no-usage-statistics',\n ]),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', [\n '/Users/me/projects/my-app',\n ]),\n },\n});\n```\n\n### HTTP (Remote MCP Endpoint)\n\nConnects to an MCP server over Streamable HTTP:\n\n```typescript\nComputer.http(url: string, options?: {\n headers?: Record<string, string>;\n})\n```\n\nUse this for MCP servers running as HTTP services:\n\n```typescript\nconst computer = new Computer({\n mcpServers: {\n docs: Computer.http('http://localhost:3001/mcp', {\n headers: { Authorization: 'Bearer token' },\n }),\n },\n});\n```\n\n### Shell (Built-in)\n\nProvides shell command execution without spawning an MCP subprocess:\n\n```typescript\nComputer.shell(options: {\n cwd?: string;\n mode: ShellMode;\n timeout?: number; // Default: 300,000ms (5 minutes)\n})\n```\n\nThis exposes a `run_command` tool (namespaced as `shell__run_command` when the key is `shell`). Commands execute in a login shell with the user's full environment.\n\n```typescript\nconst computer = new Computer({\n mcpServers: {\n shell: Computer.shell({\n cwd: '/Users/me/projects/my-app',\n mode: 'unrestricted',\n timeout: 300_000,\n }),\n },\n});\n```\n\n#### Shell Safety Modes\n\n| Mode | Description |\n| -------------------------------------- | --------------------------------------------- |\n| `'unrestricted'` | All commands allowed (for dedicated machines) |\n| `{ allowedPatterns, blockedPatterns }` | Pattern-based command filtering |\n\nPattern-based filtering:\n\n```typescript\nComputer.shell({\n cwd: workspaceDir,\n mode: {\n blockedPatterns: [/rm\\s+-rf/, /sudo/],\n allowedPatterns: [/^git\\s/, /^npm\\s/, /^ls\\s/],\n },\n});\n```\n\nWhen `allowedPatterns` is set, only matching commands are permitted. When `blockedPatterns` is set, matching commands are rejected. Blocked patterns are checked first.\n\n## Lifecycle\n\n### Starting\n\n`computer.start()` connects to all configured MCP servers in parallel. If some servers fail to connect, the computer still starts with the remaining servers - only if _all_ connections fail does it throw an error.\n\n```typescript\nconst { errors } = await computer.start();\n\nif (errors.length > 0) {\n console.warn('Some MCP servers failed to connect:', errors);\n}\n```\n\n### Stopping\n\n`computer.stop()` closes all MCP connections and kills managed processes:\n\n```typescript\nawait computer.stop();\n```\n\nAlways call `stop()` when the session ends to clean up MCP subprocesses. For managed processes (like Chrome), pass them in the config for automatic cleanup.\n\n## Dynamic Entries\n\nYou can add or remove MCP entries on a running `Computer` after `start()` has returned. This is useful when MCP configurations arrive after construction - for example, when a session-manager receives per-session entries from a dispatch payload and wants to wire them into the existing computer instead of rebuilding it.\n\n### `addEntry(namespace, entry, options?)`\n\nRegisters a new MCP entry under `namespace`. By default, connects immediately:\n\n```typescript\nawait computer.addEntry(\n 'github',\n Computer.stdio('@modelcontextprotocol/server-github', [], {\n env: { GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GH_TOKEN! },\n }),\n);\n```\n\nPass `{ deferred: true }` to register the entry without connecting. The entry starts in a degraded state and connects on the next `restartEntry(namespace)` call - useful for lazy MCPs the agent activates on demand:\n\n```typescript\nawait computer.addEntry('github', githubEntry, { deferred: true });\n\n// Later, when the agent decides it needs GitHub:\nawait computer.restartEntry('github');\n```\n\n`addEntry` throws if the namespace already exists. To replace an entry, call `removeEntry` first.\n\nIf the immediate connection fails, `addEntry` does not throw - the entry is registered as degraded with the error message attached. Inspect via `getHealth()` or `restartEntry()` to retry.\n\n### `removeEntry(namespace)`\n\nCloses the entry's connection (if any) and drops it from the configuration. No-op when the namespace doesn't exist:\n\n```typescript\nawait computer.removeEntry('github');\n```\n\n### `restartEntry(namespace)`\n\nCloses the existing connection (if any) and reconnects with the current configuration:\n\n```typescript\nawait computer.restartEntry('github');\n```\n\nUse this to bring a deferred entry online for the first time, or to recover an entry that became degraded mid-session.\n\n### Detecting dynamic-entry support\n\nConsumers that work with arbitrary `ToolProvider` implementations can detect dynamic-entry capability with `isDynamicMcpProvider`:\n\n```typescript\nimport { isDynamicMcpProvider } from '@octavus/server-sdk';\n\nif (isDynamicMcpProvider(provider)) {\n await provider.addEntry('github', githubEntry);\n}\n```\n\n`Computer` always passes this check.\n\n## Chrome Launch Helper\n\nFor desktop applications that need to control a browser, `Computer.launchChrome()` launches Chrome with remote debugging enabled:\n\n```typescript\nconst browser = await Computer.launchChrome({\n profileDir: '/Users/me/.my-app/chrome-profiles/agent-1',\n debuggingPort: 9222, // Optional, auto-allocated if omitted\n flags: ['--window-size=1280,800'],\n});\n\nconsole.log(`Chrome running on port ${browser.port}, PID ${browser.pid}`);\n```\n\nPass the browser to `managedProcesses` for automatic cleanup when the computer stops:\n\n```typescript\nconst computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', [\n `--browser-url=http://127.0.0.1:${browser.port}`,\n ]),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', [workspaceDir]),\n shell: Computer.shell({ cwd: workspaceDir, mode: 'unrestricted' }),\n },\n managedProcesses: [{ process: browser.process }],\n});\n```\n\n### ChromeLaunchOptions\n\n| Field | Required | Description |\n| --------------- | -------- | ----------------------------------------------------- |\n| `profileDir` | Yes | Directory for Chrome's user data (profile isolation) |\n| `debuggingPort` | No | Port for remote debugging (auto-allocated if omitted) |\n| `flags` | No | Additional Chrome launch flags |\n\n## ToolProvider Interface\n\n`Computer` implements the `ToolProvider` interface from `@octavus/core`:\n\n```typescript\ninterface ToolProvider {\n toolHandlers(): Record<string, ToolHandler>;\n toolSchemas(): ToolSchema[];\n}\n```\n\n`setDynamicTools()` accepts any `ToolProvider` directly - the session extracts schemas and handlers automatically:\n\n```typescript\nsession.setDynamicTools(computer);\n```\n\nYou can also pass a custom `ToolProvider`:\n\n```typescript\nconst customProvider: ToolProvider = {\n toolHandlers() {\n return {\n custom__my_tool: async (args) => {\n return { result: 'done' };\n },\n };\n },\n toolSchemas() {\n return [\n {\n name: 'custom__my_tool',\n description: 'A custom tool',\n inputSchema: {\n type: 'object',\n properties: {\n input: { type: 'string', description: 'Tool input' },\n },\n required: ['input'],\n },\n },\n ];\n },\n};\n\nconst session = client.agentSessions.attach(sessionId, {\n tools: { 'set-chat-title': titleHandler },\n});\n\nsession.setDynamicTools(customProvider);\n```\n\nFor cases where you need explicit control, `setDynamicTools()` also accepts a `DynamicTool[]` array:\n\n```typescript\ninterface DynamicTool {\n schema: ToolSchema;\n handler: ToolHandler;\n}\n```\n\n## Complete Example\n\nA desktop application with browser, filesystem, and shell capabilities:\n\n```typescript\nimport { Computer } from '@octavus/computer';\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst WORKSPACE_DIR = '/Users/me/projects/my-app';\nconst PROFILE_DIR = '/Users/me/.my-app/chrome-profiles/agent';\n\nasync function startSession(sessionId: string) {\n // 1. Launch Chrome with remote debugging\n const browser = await Computer.launchChrome({\n profileDir: PROFILE_DIR,\n });\n\n // 2. Create computer with all capabilities\n const computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', [\n `--browser-url=http://127.0.0.1:${browser.port}`,\n '--no-usage-statistics',\n ]),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', [WORKSPACE_DIR]),\n shell: Computer.shell({\n cwd: WORKSPACE_DIR,\n mode: 'unrestricted',\n }),\n },\n managedProcesses: [{ process: browser.process }],\n });\n\n // 3. Connect to all MCP servers\n const { errors } = await computer.start();\n if (errors.length > 0) {\n console.warn('Failed to connect:', errors);\n }\n\n // 4. Attach to session and register dynamic tools\n const client = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n });\n\n const session = client.agentSessions.attach(sessionId, {\n tools: {\n 'set-chat-title': async (args) => {\n console.log('Chat title:', args.title);\n return { success: true };\n },\n },\n });\n\n session.setDynamicTools(computer);\n\n // 5. Execute and stream\n const events = session.execute({\n type: 'trigger',\n triggerName: 'user-message',\n input: { USER_MESSAGE: 'Navigate to github.com and take a screenshot' },\n });\n\n for await (const event of events) {\n // Handle stream events\n }\n\n // 6. Clean up\n await computer.stop();\n}\n```\n\n## API Reference\n\n### Computer\n\n```typescript\nclass Computer implements ToolProvider {\n constructor(config: ComputerConfig);\n\n // Static factories for MCP entries\n static stdio(\n command: string,\n args?: string[],\n options?: {\n env?: Record<string, string>;\n cwd?: string;\n },\n ): StdioConfig;\n\n static http(\n url: string,\n options?: {\n headers?: Record<string, string>;\n },\n ): HttpConfig;\n\n static shell(options: { cwd?: string; mode: ShellMode; timeout?: number }): ShellConfig;\n\n // Chrome launch helper\n static launchChrome(options: ChromeLaunchOptions): Promise<ChromeInstance>;\n\n // Lifecycle\n start(): Promise<{ errors: string[] }>;\n stop(): Promise<void>;\n\n // Dynamic entries\n addEntry(namespace: string, entry: McpEntry, options?: { deferred?: boolean }): Promise<void>;\n removeEntry(namespace: string): Promise<void>;\n restartEntry(namespace: string): Promise<void>;\n stopEntry(namespace: string): Promise<void>;\n\n // Health\n getHealth(): Promise<ComputerHealth>;\n ensureReady(): Promise<EnsureReadyResult>;\n retryDegraded(): Promise<{ recovered: string[]; stillDegraded: string[] }>;\n\n // ToolProvider implementation\n toolHandlers(): Record<string, ToolHandler>;\n toolSchemas(): ToolSchema[];\n}\n\ninterface ComputerHealth {\n healthy: boolean;\n entries: EntryHealth[];\n totalTools: number;\n}\n\ninterface EntryHealth {\n name: string;\n healthy: boolean;\n error?: string;\n}\n\ninterface EnsureReadyResult extends ComputerHealth {\n recovered?: string[];\n failedEntries?: string[];\n}\n```\n\n### ComputerConfig\n\n```typescript\ninterface ComputerConfig {\n mcpServers: Record<string, McpEntry>;\n managedProcesses?: { process: ChildProcess }[];\n /** Namespaces to skip during start() - they begin as degraded and can be connected on demand via restartEntry(). */\n deferredEntries?: string[];\n}\n\ntype McpEntry = StdioConfig | HttpConfig | ShellConfig;\ntype ShellMode =\n | 'unrestricted'\n | {\n allowedPatterns?: RegExp[];\n blockedPatterns?: RegExp[];\n };\n```\n\n### ChromeInstance\n\n```typescript\ninterface ChromeInstance {\n port: number;\n process: ChildProcess;\n pid: number;\n}\n```\n",
|
|
834
|
+
content: "\n# Computer\n\nThe `@octavus/computer` package gives agents access to a physical or virtual machine's browser, filesystem, and shell. It connects to [MCP](https://modelcontextprotocol.io) servers, discovers their tools, and provides them to the server-sdk.\n\n**Current version:** `3.5.0`\n\n## Installation\n\n```bash\nnpm install @octavus/computer\n```\n\n## Quick Start\n\n```typescript\nimport { Computer } from '@octavus/computer';\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', ['--browser-url=http://127.0.0.1:9222']),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', ['/path/to/workspace']),\n shell: Computer.shell({ cwd: '/path/to/workspace', mode: 'unrestricted' }),\n },\n});\n\nawait computer.start();\n\nconst client = new OctavusClient({\n baseUrl: 'https://octavus.ai',\n apiKey: 'your-api-key',\n});\n\nconst session = client.agentSessions.attach(sessionId, {\n tools: {\n 'set-chat-title': async (args) => ({ title: args.title }),\n },\n});\n\nsession.setDynamicTools(computer);\n```\n\nDynamic tools are registered after attaching via `session.setDynamicTools()`. Pass the `computer` directly - the session extracts schemas and handlers from the `ToolProvider`. Tool schemas are sent to the platform on the next `execute()` call, and tool calls flow back through the existing execution loop.\n\n## How It Works\n\n1. You configure MCP servers with namespaces (e.g., `browser`, `filesystem`, `shell`)\n2. `computer.start()` connects to all servers in parallel and discovers their tools\n3. Each tool is namespaced with `__` (e.g., `browser__navigate_page`, `filesystem__read_file`)\n4. The server-sdk sends tool schemas to the platform and handles tool call execution\n\nThe agent's protocol must declare matching `mcpServers` with `source: device` - see [MCP Servers](/docs/protocol/mcp-servers).\n\n## Entry Types\n\nThe `Computer` class supports three types of MCP entries:\n\n### Stdio (MCP Subprocess)\n\nSpawns an MCP server as a child process, communicating via stdin/stdout:\n\n```typescript\nComputer.stdio(command: string, args?: string[], options?: {\n env?: Record<string, string>;\n cwd?: string;\n})\n```\n\nUse this for local MCP servers installed as npm packages or standalone executables:\n\n```typescript\nconst computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', [\n '--browser-url=http://127.0.0.1:9222',\n '--no-usage-statistics',\n ]),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', [\n '/Users/me/projects/my-app',\n ]),\n },\n});\n```\n\n### HTTP (Remote MCP Endpoint)\n\nConnects to an MCP server over Streamable HTTP:\n\n```typescript\nComputer.http(url: string, options?: {\n headers?: Record<string, string>;\n})\n```\n\nUse this for MCP servers running as HTTP services:\n\n```typescript\nconst computer = new Computer({\n mcpServers: {\n docs: Computer.http('http://localhost:3001/mcp', {\n headers: { Authorization: 'Bearer token' },\n }),\n },\n});\n```\n\n### Shell (Built-in)\n\nProvides shell command execution without spawning an MCP subprocess:\n\n```typescript\nComputer.shell(options: {\n cwd?: string;\n mode: ShellMode;\n timeout?: number; // Default: 300,000ms (5 minutes)\n})\n```\n\nThis exposes a `run_command` tool (namespaced as `shell__run_command` when the key is `shell`). Commands execute in a login shell with the user's full environment.\n\n```typescript\nconst computer = new Computer({\n mcpServers: {\n shell: Computer.shell({\n cwd: '/Users/me/projects/my-app',\n mode: 'unrestricted',\n timeout: 300_000,\n }),\n },\n});\n```\n\n#### Shell Safety Modes\n\n| Mode | Description |\n| -------------------------------------- | --------------------------------------------- |\n| `'unrestricted'` | All commands allowed (for dedicated machines) |\n| `{ allowedPatterns, blockedPatterns }` | Pattern-based command filtering |\n\nPattern-based filtering:\n\n```typescript\nComputer.shell({\n cwd: workspaceDir,\n mode: {\n blockedPatterns: [/rm\\s+-rf/, /sudo/],\n allowedPatterns: [/^git\\s/, /^npm\\s/, /^ls\\s/],\n },\n});\n```\n\nWhen `allowedPatterns` is set, only matching commands are permitted. When `blockedPatterns` is set, matching commands are rejected. Blocked patterns are checked first.\n\n## Lifecycle\n\n### Starting\n\n`computer.start()` connects to all configured MCP servers in parallel. If some servers fail to connect, the computer still starts with the remaining servers - only if _all_ connections fail does it throw an error.\n\n```typescript\nconst { errors } = await computer.start();\n\nif (errors.length > 0) {\n console.warn('Some MCP servers failed to connect:', errors);\n}\n```\n\n### Stopping\n\n`computer.stop()` closes all MCP connections and kills managed processes:\n\n```typescript\nawait computer.stop();\n```\n\nAlways call `stop()` when the session ends to clean up MCP subprocesses. For managed processes (like Chrome), pass them in the config for automatic cleanup.\n\n## Dynamic Entries\n\nYou can add or remove MCP entries on a running `Computer` after `start()` has returned. This is useful when MCP configurations arrive after construction - for example, when a session-manager receives per-session entries from a dispatch payload and wants to wire them into the existing computer instead of rebuilding it.\n\n### `addEntry(namespace, entry, options?)`\n\nRegisters a new MCP entry under `namespace`. By default, connects immediately:\n\n```typescript\nawait computer.addEntry(\n 'github',\n Computer.stdio('@modelcontextprotocol/server-github', [], {\n env: { GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GH_TOKEN! },\n }),\n);\n```\n\nPass `{ deferred: true }` to register the entry without connecting. The entry starts in a degraded state and connects on the next `restartEntry(namespace)` call - useful for lazy MCPs the agent activates on demand:\n\n```typescript\nawait computer.addEntry('github', githubEntry, { deferred: true });\n\n// Later, when the agent decides it needs GitHub:\nawait computer.restartEntry('github');\n```\n\n`addEntry` throws if the namespace already exists. To replace an entry, call `removeEntry` first.\n\nIf the immediate connection fails, `addEntry` does not throw - the entry is registered as degraded with the error message attached. Inspect via `getHealth()` or `restartEntry()` to retry.\n\n### `removeEntry(namespace)`\n\nCloses the entry's connection (if any) and drops it from the configuration. No-op when the namespace doesn't exist:\n\n```typescript\nawait computer.removeEntry('github');\n```\n\n### `restartEntry(namespace)`\n\nCloses the existing connection (if any) and reconnects with the current configuration:\n\n```typescript\nawait computer.restartEntry('github');\n```\n\nUse this to bring a deferred entry online for the first time, or to recover an entry that became degraded mid-session.\n\n### Detecting dynamic-entry support\n\nConsumers that work with arbitrary `ToolProvider` implementations can detect dynamic-entry capability with `isDynamicMcpProvider`:\n\n```typescript\nimport { isDynamicMcpProvider } from '@octavus/server-sdk';\n\nif (isDynamicMcpProvider(provider)) {\n await provider.addEntry('github', githubEntry);\n}\n```\n\n`Computer` always passes this check.\n\n## Chrome Launch Helper\n\nFor desktop applications that need to control a browser, `Computer.launchChrome()` launches Chrome with remote debugging enabled:\n\n```typescript\nconst browser = await Computer.launchChrome({\n profileDir: '/Users/me/.my-app/chrome-profiles/agent-1',\n debuggingPort: 9222, // Optional, auto-allocated if omitted\n flags: ['--window-size=1280,800'],\n});\n\nconsole.log(`Chrome running on port ${browser.port}, PID ${browser.pid}`);\n```\n\nPass the browser to `managedProcesses` for automatic cleanup when the computer stops:\n\n```typescript\nconst computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', [\n `--browser-url=http://127.0.0.1:${browser.port}`,\n ]),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', [workspaceDir]),\n shell: Computer.shell({ cwd: workspaceDir, mode: 'unrestricted' }),\n },\n managedProcesses: [{ process: browser.process }],\n});\n```\n\n### ChromeLaunchOptions\n\n| Field | Required | Description |\n| --------------- | -------- | ----------------------------------------------------- |\n| `profileDir` | Yes | Directory for Chrome's user data (profile isolation) |\n| `debuggingPort` | No | Port for remote debugging (auto-allocated if omitted) |\n| `flags` | No | Additional Chrome launch flags |\n\n## ToolProvider Interface\n\n`Computer` implements the `ToolProvider` interface from `@octavus/core`:\n\n```typescript\ninterface ToolProvider {\n toolHandlers(): Record<string, ToolHandler>;\n toolSchemas(): ToolSchema[];\n}\n```\n\n`setDynamicTools()` accepts any `ToolProvider` directly - the session extracts schemas and handlers automatically:\n\n```typescript\nsession.setDynamicTools(computer);\n```\n\nYou can also pass a custom `ToolProvider`:\n\n```typescript\nconst customProvider: ToolProvider = {\n toolHandlers() {\n return {\n custom__my_tool: async (args) => {\n return { result: 'done' };\n },\n };\n },\n toolSchemas() {\n return [\n {\n name: 'custom__my_tool',\n description: 'A custom tool',\n inputSchema: {\n type: 'object',\n properties: {\n input: { type: 'string', description: 'Tool input' },\n },\n required: ['input'],\n },\n },\n ];\n },\n};\n\nconst session = client.agentSessions.attach(sessionId, {\n tools: { 'set-chat-title': titleHandler },\n});\n\nsession.setDynamicTools(customProvider);\n```\n\nFor cases where you need explicit control, `setDynamicTools()` also accepts a `DynamicTool[]` array:\n\n```typescript\ninterface DynamicTool {\n schema: ToolSchema;\n handler: ToolHandler;\n}\n```\n\n## Complete Example\n\nA desktop application with browser, filesystem, and shell capabilities:\n\n```typescript\nimport { Computer } from '@octavus/computer';\nimport { OctavusClient } from '@octavus/server-sdk';\n\nconst WORKSPACE_DIR = '/Users/me/projects/my-app';\nconst PROFILE_DIR = '/Users/me/.my-app/chrome-profiles/agent';\n\nasync function startSession(sessionId: string) {\n // 1. Launch Chrome with remote debugging\n const browser = await Computer.launchChrome({\n profileDir: PROFILE_DIR,\n });\n\n // 2. Create computer with all capabilities\n const computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', [\n `--browser-url=http://127.0.0.1:${browser.port}`,\n '--no-usage-statistics',\n ]),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', [WORKSPACE_DIR]),\n shell: Computer.shell({\n cwd: WORKSPACE_DIR,\n mode: 'unrestricted',\n }),\n },\n managedProcesses: [{ process: browser.process }],\n });\n\n // 3. Connect to all MCP servers\n const { errors } = await computer.start();\n if (errors.length > 0) {\n console.warn('Failed to connect:', errors);\n }\n\n // 4. Attach to session and register dynamic tools\n const client = new OctavusClient({\n baseUrl: process.env.OCTAVUS_API_URL!,\n apiKey: process.env.OCTAVUS_API_KEY!,\n });\n\n const session = client.agentSessions.attach(sessionId, {\n tools: {\n 'set-chat-title': async (args) => {\n console.log('Chat title:', args.title);\n return { success: true };\n },\n },\n });\n\n session.setDynamicTools(computer);\n\n // 5. Execute and stream\n const events = session.execute({\n type: 'trigger',\n triggerName: 'user-message',\n input: { USER_MESSAGE: 'Navigate to github.com and take a screenshot' },\n });\n\n for await (const event of events) {\n // Handle stream events\n }\n\n // 6. Clean up\n await computer.stop();\n}\n```\n\n## API Reference\n\n### Computer\n\n```typescript\nclass Computer implements ToolProvider {\n constructor(config: ComputerConfig);\n\n // Static factories for MCP entries\n static stdio(\n command: string,\n args?: string[],\n options?: {\n env?: Record<string, string>;\n cwd?: string;\n },\n ): StdioConfig;\n\n static http(\n url: string,\n options?: {\n headers?: Record<string, string>;\n },\n ): HttpConfig;\n\n static shell(options: { cwd?: string; mode: ShellMode; timeout?: number }): ShellConfig;\n\n // Chrome launch helper\n static launchChrome(options: ChromeLaunchOptions): Promise<ChromeInstance>;\n\n // Lifecycle\n start(): Promise<{ errors: string[] }>;\n stop(): Promise<void>;\n\n // Dynamic entries\n addEntry(namespace: string, entry: McpEntry, options?: { deferred?: boolean }): Promise<void>;\n removeEntry(namespace: string): Promise<void>;\n restartEntry(namespace: string): Promise<void>;\n stopEntry(namespace: string): Promise<void>;\n\n // Health\n getHealth(): Promise<ComputerHealth>;\n ensureReady(): Promise<EnsureReadyResult>;\n retryDegraded(): Promise<{ recovered: string[]; stillDegraded: string[] }>;\n\n // ToolProvider implementation\n toolHandlers(): Record<string, ToolHandler>;\n toolSchemas(): ToolSchema[];\n}\n\ninterface ComputerHealth {\n healthy: boolean;\n entries: EntryHealth[];\n totalTools: number;\n}\n\ninterface EntryHealth {\n name: string;\n healthy: boolean;\n error?: string;\n}\n\ninterface EnsureReadyResult extends ComputerHealth {\n recovered?: string[];\n failedEntries?: string[];\n}\n```\n\n### ComputerConfig\n\n```typescript\ninterface ComputerConfig {\n mcpServers: Record<string, McpEntry>;\n managedProcesses?: { process: ChildProcess }[];\n /** Namespaces to skip during start() - they begin as degraded and can be connected on demand via restartEntry(). */\n deferredEntries?: string[];\n}\n\ntype McpEntry = StdioConfig | HttpConfig | ShellConfig;\ntype ShellMode =\n | 'unrestricted'\n | {\n allowedPatterns?: RegExp[];\n blockedPatterns?: RegExp[];\n };\n```\n\n### ChromeInstance\n\n```typescript\ninterface ChromeInstance {\n port: number;\n process: ChildProcess;\n pid: number;\n}\n```\n",
|
|
826
835
|
excerpt: "Computer The package gives agents access to a physical or virtual machine's browser, filesystem, and shell. It connects to MCP servers, discovers their tools, and provides them to the server-sdk....",
|
|
827
836
|
order: 8
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
slug: "server-sdk/inline-mcp",
|
|
840
|
+
section: "server-sdk",
|
|
841
|
+
title: "Inline MCP Servers",
|
|
842
|
+
description: "Group an integration's tools into a Zod-typed bundle that runs in your server process.",
|
|
843
|
+
content: "\n# Inline MCP Servers\n\nInline MCP servers let you group an integration's tools (e.g., GitHub, Salesforce, an internal microservice) into a namespaced bundle with Zod-typed handler arguments. The tools execute in your server process via the same tool-request/continue path as ordinary [server tools](/docs/server-sdk/tools), so authentication and credentials stay in your process.\n\n## When to Use\n\nUse an inline MCP server when:\n\n- You're integrating a third-party API and want a logical grouping (`github__list-prs`, `github__get-issue`).\n- You want type-safe handler arguments instead of casting `args` from `unknown`.\n- You want to evolve the toolset without protocol-yaml round trips - tool names and schemas are sent at runtime.\n- Tool calls need credentials your platform should never see (OAuth tokens, customer API keys).\n\nFor comparison with the other tool registration paths, see the [tools overview](/docs/server-sdk/tools#server-tools-vs-client-tools).\n\n## Protocol Declaration\n\nDeclare the namespace in `protocol.yaml` with `source: consumer`. The platform learns the namespace and routes tool calls to your process; tool names and JSON schemas are provided by the SDK at runtime.\n\n```yaml\nmcpServers:\n github:\n description: Repository management - issues, pull requests, code\n source: consumer\n display: name\n\nagent:\n mcpServers:\n - github\n```\n\nSee [MCP Servers in the protocol reference](/docs/protocol/mcp-servers) for the full set of MCP source types and field semantics.\n\n## Defining the Server\n\n```typescript\nimport { z } from 'zod';\nimport { createInlineMcpServer, defineInlineMcpTool } from '@octavus/server-sdk';\n\nconst github = createInlineMcpServer('github', {\n tools: {\n 'get-pr-overview': defineInlineMcpTool({\n description: 'Get pull request metadata and file changes',\n parameters: z.object({\n owner: z.string(),\n repo: z.string(),\n pullNumber: z.number(),\n }),\n handler: async (args) => {\n // args is { owner: string; repo: string; pullNumber: number }\n return await githubService.getPrOverview(args.owner, args.repo, args.pullNumber);\n },\n }),\n\n 'list-issues': defineInlineMcpTool({\n description: 'List open issues for a repository',\n parameters: z.object({\n owner: z.string(),\n repo: z.string(),\n state: z.enum(['open', 'closed', 'all']).default('open'),\n }),\n handler: async (args) => {\n return await githubService.listIssues(args.owner, args.repo, args.state);\n },\n }),\n },\n});\n```\n\nThe factory:\n\n1. Validates the namespace and each tool name (see [naming rules](#naming-rules)).\n2. Converts each Zod schema to JSON Schema once at creation time.\n3. Returns an `InlineMcpServer` exposing `toolSchemas()` and `toolHandlers()`.\n\nThe resulting tool names are namespaced: `github__get-pr-overview`, `github__list-issues`.\n\n## Why `defineInlineMcpTool`\n\n`defineInlineMcpTool()` is a no-op at runtime, but it preserves Zod type inference. Without the wrapper, TypeScript collapses the per-tool generic when the tools are placed in a record literal, leaving `args` typed as `unknown`:\n\n```typescript\n// Without defineInlineMcpTool - args ends up as `unknown`\ntools: {\n 'get-pr-overview': {\n description: '...',\n parameters: z.object({ owner: z.string() }),\n handler: async (args) => args.owner, // \u274C TS error: args is 'unknown'\n },\n}\n\n// With defineInlineMcpTool - args inferred from the schema\ntools: {\n 'get-pr-overview': defineInlineMcpTool({\n description: '...',\n parameters: z.object({ owner: z.string() }),\n handler: async (args) => args.owner, // \u2713 args is { owner: string }\n }),\n}\n```\n\nThe handler also receives Zod-validated arguments. Invalid inputs throw before reaching your code, with the failed paths and messages joined into the error.\n\n## Attaching to a Session\n\nPass inline MCP servers via `mcpServers` on `attach()`. They merge with `tools` and survive across `setDynamicTools()` calls:\n\n```typescript\nconst session = client.agentSessions.attach(sessionId, {\n tools: {\n 'get-user-account': async (args) => db.users.findById(args.userId as string),\n },\n mcpServers: [github, salesforce],\n});\n```\n\nWorkers accept the same option:\n\n```typescript\nconst { output } = await client.workers.generate(agentId, input, {\n mcpServers: [github],\n});\n```\n\n## Authentication and Credentials\n\nHandlers close over your server's auth context, so credentials never leave your process. The platform receives the namespaced schema list and the tool call name; it never sees the keys you use to fulfill the call.\n\nA common pattern is to build the MCP server per-request when auth depends on the user:\n\n```typescript\nfunction buildGithubMcp(token: string) {\n const client = new GithubClient({ token });\n return createInlineMcpServer('github', {\n tools: {\n 'get-pr-overview': defineInlineMcpTool({\n description: 'Get pull request metadata and file changes',\n parameters: z.object({\n owner: z.string(),\n repo: z.string(),\n pullNumber: z.number(),\n }),\n handler: async (args) => client.pulls.get(args),\n }),\n },\n });\n}\n\nexport async function POST(request: Request) {\n const user = await authenticate(request);\n const session = client.agentSessions.attach(sessionId, {\n mcpServers: [buildGithubMcp(user.githubToken)],\n });\n\n const events = session.execute(payload, { signal: request.signal });\n return new Response(toSSEStream(events));\n}\n```\n\nFor static credentials (one tenant per deployment), build the server once at module scope.\n\n## How Tool Calls Flow\n\n1. The agent emits a `tool-request` event for `github__get-pr-overview` (or another inline-MCP-namespaced tool).\n2. The Server SDK looks up the handler registered by `createInlineMcpServer()` and runs it. Zod validates `args` against the tool's schema; a validation failure becomes a tool error sent back to the LLM.\n3. The handler returns; the SDK posts the result back to the platform via the same continuation request used for ordinary server tools.\n4. The platform feeds the result to the LLM and streams the next response chunk.\n\nThere is no separate transport - inline MCP tools ride on the same `dynamicToolSchemas` channel that device MCPs use, so no additional infrastructure is required.\n\n## Naming Rules\n\n`createInlineMcpServer()` validates the namespace and each tool name at construction time. Invalid values throw immediately:\n\n- **Namespace:** lowercase letters, digits, and hyphens; must start with a letter (`/^[a-z][a-z0-9-]*$/`).\n- **Tool name:** lowercase letters, digits, underscores, and hyphens; must start with a letter (`/^[a-z][a-z0-9_-]*$/`).\n\nThe resulting `${namespace}__${toolName}` is what the LLM sees and what flows through the platform's MCP routing.\n\n## Collision Rules\n\nThe resolver throws on the following conflicts so problems surface at attach time, not mid-stream:\n\n- Each inline MCP server's `namespace` must be unique across the array passed to `attach()` or the workers API.\n- A namespaced tool name (`namespace__tool`) cannot collide with a static tool handler key passed via `tools`.\n- A namespaced tool name cannot collide with a `dynamicToolSchemas` entry passed to the workers API.\n\nIf a tool registered via `setDynamicTools()` later collides with an inline MCP tool name, the dynamic handler wins for the duration of that dynamic-tool set; the inline MCP handler is restored on the next `setDynamicTools()` call that doesn't re-register the same name.\n\n## Inline vs Computer\n\nBoth inline MCP and the `Computer` integration register tools that flow through `dynamicToolSchemas`. Pick based on where the tool process runs:\n\n| Tool surface | Process location | Best for |\n| ------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------- |\n| Inline MCP | Your server (in-process closure) | Third-party APIs, internal microservices, anything where credentials live in your backend. |\n| Computer | The agent's machine (STDIO MCP) | Browser automation, filesystem, shell - device-local capabilities. See [Computer](/docs/server-sdk/computer). |\n\nThe two can coexist on the same session.\n",
|
|
844
|
+
excerpt: "Inline MCP Servers Inline MCP servers let you group an integration's tools (e.g., GitHub, Salesforce, an internal microservice) into a namespaced bundle with Zod-typed handler arguments. The tools...",
|
|
845
|
+
order: 9
|
|
828
846
|
}
|
|
829
847
|
]
|
|
830
848
|
},
|
|
@@ -839,7 +857,7 @@ var sections_default = [
|
|
|
839
857
|
section: "client-sdk",
|
|
840
858
|
title: "Overview",
|
|
841
859
|
description: "Introduction to the Octavus Client SDKs for building chat interfaces.",
|
|
842
|
-
content: "\n# Client SDK Overview\n\nOctavus provides two packages for frontend integration:\n\n| Package | Purpose | Use When |\n| --------------------- | ------------------------ | ----------------------------------------------------- |\n| `@octavus/react` | React hooks and bindings | Building React applications |\n| `@octavus/client-sdk` | Framework-agnostic core | Using Vue, Svelte, vanilla JS, or custom integrations |\n\n**Most users should install `@octavus/react`** - it includes everything from `@octavus/client-sdk` plus React-specific hooks.\n\n## Installation\n\n### React Applications\n\n```bash\nnpm install @octavus/react\n```\n\n**Current version:** `3.3.0`\n\n### Other Frameworks\n\n```bash\nnpm install @octavus/client-sdk\n```\n\n**Current version:** `3.3.0`\n\n## Transport Pattern\n\nThe Client SDK uses a **transport abstraction** to handle communication with your backend. This gives you flexibility in how events are delivered:\n\n| Transport | Use Case | Docs |\n| ----------------------- | -------------------------------------------- | ----------------------------------------------------- |\n| `createHttpTransport` | HTTP/SSE (Next.js, Express, etc.) | [HTTP Transport](/docs/client-sdk/http-transport) |\n| `createSocketTransport` | WebSocket, SockJS, or other socket protocols | [Socket Transport](/docs/client-sdk/socket-transport) |\n\nWhen the transport changes (e.g., when `sessionId` changes), the `useOctavusChat` hook automatically reinitializes with the new transport.\n\n> **Recommendation**: Use HTTP transport unless you specifically need WebSocket features (custom real-time events, Meteor/Phoenix, etc.).\n\n## React Usage\n\nThe `useOctavusChat` hook provides state management and streaming for React applications:\n\n```tsx\nimport { useMemo } from 'react';\nimport { useOctavusChat, createHttpTransport, type UIMessage } from '@octavus/react';\n\nfunction Chat({ sessionId }: { sessionId: string }) {\n // Create a stable transport instance (memoized on sessionId)\n const transport = useMemo(\n () =>\n createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n }),\n [sessionId],\n );\n\n const { messages, status, send } = useOctavusChat({ transport });\n\n const sendMessage = async (text: string) => {\n await send('user-message', { USER_MESSAGE: text }, { userMessage: { content: text } });\n };\n\n return (\n <div>\n {messages.map((msg) => (\n <MessageBubble key={msg.id} message={msg} />\n ))}\n </div>\n );\n}\n\nfunction MessageBubble({ message }: { message: UIMessage }) {\n return (\n <div>\n {message.parts.map((part, i) => {\n if (part.type === 'text') {\n return <p key={i}>{part.text}</p>;\n }\n return null;\n })}\n </div>\n );\n}\n```\n\n## Framework-Agnostic Usage\n\nThe `OctavusChat` class can be used with any framework or vanilla JavaScript:\n\n```typescript\nimport { OctavusChat, createHttpTransport } from '@octavus/client-sdk';\n\nconst transport = createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n});\n\nconst chat = new OctavusChat({ transport });\n\n// Subscribe to state changes\nconst unsubscribe = chat.subscribe(() => {\n console.log('Messages:', chat.messages);\n console.log('Status:', chat.status);\n // Update your UI here\n});\n\n// Send a message\nawait chat.send('user-message', { USER_MESSAGE: 'Hello' }, { userMessage: { content: 'Hello' } });\n\n// Cleanup when done\nunsubscribe();\n```\n\n## Key Features\n\n### Unified Send Function\n\nThe `send` function handles both user message display and agent triggering in one call:\n\n```tsx\nconst { send } = useOctavusChat({ transport });\n\n// Add user message to UI and trigger agent\nawait send('user-message', { USER_MESSAGE: text }, { userMessage: { content: text } });\n\n// Trigger without adding a user message (e.g., button click)\nawait send('request-human');\n```\n\n### Message Parts\n\nMessages contain ordered `parts` for rich content:\n\n```tsx\nconst { messages } = useOctavusChat({ transport });\n\n// Each message has typed parts\nmessage.parts.map((part) => {\n switch (part.type) {\n case 'text': // Text content\n case 'reasoning': // Extended reasoning/thinking\n case 'tool-call': // Tool execution\n case 'operation': // Internal operations (set-resource, etc.)\n }\n});\n```\n\n### Status Tracking\n\n```tsx\nconst { status } = useOctavusChat({ transport });\n\n// status: 'idle' | 'streaming' | 'error' | 'awaiting-input'\n// 'awaiting-input' occurs when interactive client tools need user action\n```\n\n### Stop Streaming\n\n```tsx\nconst { stop } = useOctavusChat({ transport });\n\n// Stop current stream and finalize message\nstop();\n```\n\n### Retry Last Trigger\n\nRe-execute the last trigger from the same starting point. Messages are rolled back to the state before the trigger, the user message is re-added (if any), and the agent re-executes. Already-uploaded files are reused without re-uploading.\n\n```tsx\nconst { retry, canRetry } = useOctavusChat({ transport });\n\n// Retry after an error, cancellation, or unsatisfactory result\nif (canRetry) {\n await retry();\n}\n```\n\n`canRetry` is `true` when a trigger has been sent and the chat is not currently streaming or awaiting input.\n\n## Hook Reference (React)\n\n### useOctavusChat\n\n```typescript\nfunction useOctavusChat(options: OctavusChatOptions): UseOctavusChatReturn;\n\ninterface OctavusChatOptions {\n // Required: Transport for streaming events\n transport: Transport;\n\n // Optional: Function to request upload URLs for file uploads\n requestUploadUrls?: (\n files: { filename: string; mediaType: string; size: number }[],\n ) => Promise<UploadUrlsResponse>;\n\n // Optional: Client-side tool handlers\n // - Function: executes automatically and returns result\n // - 'interactive': appears in pendingClientTools for user input\n clientTools?: Record<string, ClientToolHandler>;\n\n // Optional: Pre-populate with existing messages (session restore)\n initialMessages?: UIMessage[];\n\n // Optional: Callbacks\n onError?: (error: OctavusError) => void; // Structured error with type, source, retryable\n onFinish?: () => void;\n onStop?: () => void; // Called when user stops generation\n onResourceUpdate?: (name: string, value: unknown) => void;\n}\n\ninterface UseOctavusChatReturn {\n // State\n messages: UIMessage[];\n status: ChatStatus; // 'idle' | 'streaming' | 'error' | 'awaiting-input'\n error: OctavusError | null; // Structured error with type, source, retryable\n\n // Connection (socket transport only - undefined for HTTP)\n connectionState: ConnectionState | undefined; // 'disconnected' | 'connecting' | 'connected' | 'error'\n connectionError: Error | undefined;\n\n // Client tools (interactive tools awaiting user input)\n pendingClientTools: Record<string, InteractiveTool[]>; // Keyed by tool name\n\n // Actions\n send: (\n triggerName: string,\n input?: Record<string, unknown>,\n options?: { userMessage?: UserMessageInput },\n ) => Promise<void>;\n stop: () => void;\n retry: () => Promise<void>; // Retry last trigger from same starting point\n canRetry: boolean; // Whether retry() can be called\n\n // Connection management (socket transport only - undefined for HTTP)\n connect: (() => Promise<void>) | undefined;\n disconnect: (() => void) | undefined;\n\n // File uploads (requires requestUploadUrls)\n uploadFiles: (\n files: FileList | File[],\n onProgress?: (fileIndex: number, progress: number) => void,\n ) => Promise<FileReference[]>;\n}\n\ninterface UserMessageInput {\n content?: string;\n files?: FileList | File[] | FileReference[];\n}\n```\n\n### useAutoScroll\n\nSmart auto-scroll for chat containers. Scrolls to bottom when content updates, but pauses if the user has scrolled up. See [Streaming - Auto-Scroll](/docs/client-sdk/streaming#auto-scroll) for full usage.\n\n```typescript\nfunction useAutoScroll(options?: UseAutoScrollOptions): {\n scrollRef: RefObject<HTMLDivElement | null>;\n handleScroll: () => void;\n scrollOnUpdate: () => void;\n resetAutoScroll: () => void;\n};\n\ninterface UseAutoScrollOptions {\n scrollRef?: RefObject<HTMLDivElement | null>;\n threshold?: number; // Distance from bottom in px (default: 80)\n}\n```\n\n## Transport Reference\n\n### createHttpTransport\n\nCreates an HTTP/SSE transport using native `fetch()`:\n\n```typescript\nimport { createHttpTransport } from '@octavus/react';\n\nconst transport = createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n});\n```\n\n### createSocketTransport\n\nCreates a WebSocket/SockJS transport for real-time connections:\n\n```typescript\nimport { createSocketTransport } from '@octavus/react';\n\nconst transport = createSocketTransport({\n connect: () =>\n new Promise((resolve, reject) => {\n const ws = new WebSocket(`wss://api.example.com/stream?sessionId=${sessionId}`);\n ws.onopen = () => resolve(ws);\n ws.onerror = () => reject(new Error('Connection failed'));\n }),\n});\n```\n\nSocket transport provides additional connection management:\n\n```typescript\n// Access connection state directly\ntransport.connectionState; // 'disconnected' | 'connecting' | 'connected' | 'error'\n\n// Subscribe to state changes\ntransport.onConnectionStateChange((state, error) => {\n /* ... */\n});\n\n// Eager connection (instead of lazy on first send)\nawait transport.connect();\n\n// Manual disconnect\ntransport.disconnect();\n```\n\nFor detailed WebSocket/SockJS usage including custom events, reconnection patterns, and server-side implementation, see [Socket Transport](/docs/client-sdk/socket-transport).\n\n## Class Reference (Framework-Agnostic)\n\n### OctavusChat\n\n```typescript\nclass OctavusChat {\n constructor(options: OctavusChatOptions);\n\n // State (read-only)\n readonly messages: UIMessage[];\n readonly status: ChatStatus; // 'idle' | 'streaming' | 'error' | 'awaiting-input'\n readonly error: OctavusError | null; // Structured error\n readonly pendingClientTools: Record<string, InteractiveTool[]>; // Interactive tools\n\n // Actions\n send(\n triggerName: string,\n input?: Record<string, unknown>,\n options?: { userMessage?: UserMessageInput },\n ): Promise<void>;\n stop(): void;\n\n // Subscription\n subscribe(callback: () => void): () => void; // Returns unsubscribe function\n}\n```\n\n## Next Steps\n\n- [HTTP Transport](/docs/client-sdk/http-transport) - HTTP/SSE integration (recommended)\n- [Socket Transport](/docs/client-sdk/socket-transport) - WebSocket and SockJS integration\n- [Messages](/docs/client-sdk/messages) - Working with message state\n- [Streaming](/docs/client-sdk/streaming) - Building streaming UIs\n- [Client Tools](/docs/client-sdk/client-tools) - Interactive browser-side tool handling\n- [Operations](/docs/client-sdk/execution-blocks) - Showing agent progress\n- [Error Handling](/docs/client-sdk/error-handling) - Handling errors with type guards\n- [File Uploads](/docs/client-sdk/file-uploads) - Uploading images and documents\n- [Examples](/docs/examples/overview) - Complete working examples\n",
|
|
860
|
+
content: "\n# Client SDK Overview\n\nOctavus provides two packages for frontend integration:\n\n| Package | Purpose | Use When |\n| --------------------- | ------------------------ | ----------------------------------------------------- |\n| `@octavus/react` | React hooks and bindings | Building React applications |\n| `@octavus/client-sdk` | Framework-agnostic core | Using Vue, Svelte, vanilla JS, or custom integrations |\n\n**Most users should install `@octavus/react`** - it includes everything from `@octavus/client-sdk` plus React-specific hooks.\n\n## Installation\n\n### React Applications\n\n```bash\nnpm install @octavus/react\n```\n\n**Current version:** `3.5.0`\n\n### Other Frameworks\n\n```bash\nnpm install @octavus/client-sdk\n```\n\n**Current version:** `3.5.0`\n\n## Transport Pattern\n\nThe Client SDK uses a **transport abstraction** to handle communication with your backend. This gives you flexibility in how events are delivered:\n\n| Transport | Use Case | Docs |\n| ----------------------- | -------------------------------------------- | ----------------------------------------------------- |\n| `createHttpTransport` | HTTP/SSE (Next.js, Express, etc.) | [HTTP Transport](/docs/client-sdk/http-transport) |\n| `createSocketTransport` | WebSocket, SockJS, or other socket protocols | [Socket Transport](/docs/client-sdk/socket-transport) |\n\nWhen the transport changes (e.g., when `sessionId` changes), the `useOctavusChat` hook automatically reinitializes with the new transport.\n\n> **Recommendation**: Use HTTP transport unless you specifically need WebSocket features (custom real-time events, Meteor/Phoenix, etc.).\n\n## React Usage\n\nThe `useOctavusChat` hook provides state management and streaming for React applications:\n\n```tsx\nimport { useMemo } from 'react';\nimport { useOctavusChat, createHttpTransport, type UIMessage } from '@octavus/react';\n\nfunction Chat({ sessionId }: { sessionId: string }) {\n // Create a stable transport instance (memoized on sessionId)\n const transport = useMemo(\n () =>\n createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n }),\n [sessionId],\n );\n\n const { messages, status, send } = useOctavusChat({ transport });\n\n const sendMessage = async (text: string) => {\n await send('user-message', { USER_MESSAGE: text }, { userMessage: { content: text } });\n };\n\n return (\n <div>\n {messages.map((msg) => (\n <MessageBubble key={msg.id} message={msg} />\n ))}\n </div>\n );\n}\n\nfunction MessageBubble({ message }: { message: UIMessage }) {\n return (\n <div>\n {message.parts.map((part, i) => {\n if (part.type === 'text') {\n return <p key={i}>{part.text}</p>;\n }\n return null;\n })}\n </div>\n );\n}\n```\n\n## Framework-Agnostic Usage\n\nThe `OctavusChat` class can be used with any framework or vanilla JavaScript:\n\n```typescript\nimport { OctavusChat, createHttpTransport } from '@octavus/client-sdk';\n\nconst transport = createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n});\n\nconst chat = new OctavusChat({ transport });\n\n// Subscribe to state changes\nconst unsubscribe = chat.subscribe(() => {\n console.log('Messages:', chat.messages);\n console.log('Status:', chat.status);\n // Update your UI here\n});\n\n// Send a message\nawait chat.send('user-message', { USER_MESSAGE: 'Hello' }, { userMessage: { content: 'Hello' } });\n\n// Cleanup when done\nunsubscribe();\n```\n\n## Key Features\n\n### Unified Send Function\n\nThe `send` function handles both user message display and agent triggering in one call:\n\n```tsx\nconst { send } = useOctavusChat({ transport });\n\n// Add user message to UI and trigger agent\nawait send('user-message', { USER_MESSAGE: text }, { userMessage: { content: text } });\n\n// Trigger without adding a user message (e.g., button click)\nawait send('request-human');\n```\n\n### Message Parts\n\nMessages contain ordered `parts` for rich content:\n\n```tsx\nconst { messages } = useOctavusChat({ transport });\n\n// Each message has typed parts\nmessage.parts.map((part) => {\n switch (part.type) {\n case 'text': // Text content\n case 'reasoning': // Extended reasoning/thinking\n case 'tool-call': // Tool execution\n case 'operation': // Internal operations (set-resource, etc.)\n }\n});\n```\n\n### Status Tracking\n\n```tsx\nconst { status } = useOctavusChat({ transport });\n\n// status: 'idle' | 'streaming' | 'error' | 'awaiting-input'\n// 'awaiting-input' occurs when interactive client tools need user action\n```\n\n### Stop Streaming\n\n```tsx\nconst { stop } = useOctavusChat({ transport });\n\n// Stop current stream and finalize message\nstop();\n```\n\n### Retry Last Trigger\n\nRe-execute the last trigger from the same starting point. Messages are rolled back to the state before the trigger, the user message is re-added (if any), and the agent re-executes. Already-uploaded files are reused without re-uploading.\n\n```tsx\nconst { retry, canRetry } = useOctavusChat({ transport });\n\n// Retry after an error, cancellation, or unsatisfactory result\nif (canRetry) {\n await retry();\n}\n```\n\n`canRetry` is `true` when a trigger has been sent and the chat is not currently streaming or awaiting input.\n\n## Hook Reference (React)\n\n### useOctavusChat\n\n```typescript\nfunction useOctavusChat(options: OctavusChatOptions): UseOctavusChatReturn;\n\ninterface OctavusChatOptions {\n // Required: Transport for streaming events\n transport: Transport;\n\n // Optional: Function to request upload URLs for file uploads\n requestUploadUrls?: (\n files: { filename: string; mediaType: string; size: number }[],\n ) => Promise<UploadUrlsResponse>;\n\n // Optional: Client-side tool handlers\n // - Function: executes automatically and returns result\n // - 'interactive': appears in pendingClientTools for user input\n clientTools?: Record<string, ClientToolHandler>;\n\n // Optional: Pre-populate with existing messages (session restore)\n initialMessages?: UIMessage[];\n\n // Optional: Callbacks\n onError?: (error: OctavusError) => void; // Structured error with type, source, retryable\n onFinish?: () => void;\n onStop?: () => void; // Called when user stops generation\n onResourceUpdate?: (name: string, value: unknown) => void;\n}\n\ninterface UseOctavusChatReturn {\n // State\n messages: UIMessage[];\n status: ChatStatus; // 'idle' | 'streaming' | 'error' | 'awaiting-input'\n error: OctavusError | null; // Structured error with type, source, retryable\n\n // Connection (socket transport only - undefined for HTTP)\n connectionState: ConnectionState | undefined; // 'disconnected' | 'connecting' | 'connected' | 'error'\n connectionError: Error | undefined;\n\n // Client tools (interactive tools awaiting user input)\n pendingClientTools: Record<string, InteractiveTool[]>; // Keyed by tool name\n\n // Actions\n send: (\n triggerName: string,\n input?: Record<string, unknown>,\n options?: { userMessage?: UserMessageInput },\n ) => Promise<void>;\n stop: () => void;\n retry: () => Promise<void>; // Retry last trigger from same starting point\n canRetry: boolean; // Whether retry() can be called\n\n // Connection management (socket transport only - undefined for HTTP)\n connect: (() => Promise<void>) | undefined;\n disconnect: (() => void) | undefined;\n\n // File uploads (requires requestUploadUrls)\n uploadFiles: (\n files: FileList | File[],\n onProgress?: (fileIndex: number, progress: number) => void,\n ) => Promise<FileReference[]>;\n}\n\ninterface UserMessageInput {\n content?: string;\n files?: FileList | File[] | FileReference[];\n}\n```\n\n### useAutoScroll\n\nSmart auto-scroll for chat containers. Scrolls to bottom when content updates, but pauses if the user has scrolled up. See [Streaming - Auto-Scroll](/docs/client-sdk/streaming#auto-scroll) for full usage.\n\n```typescript\nfunction useAutoScroll(options?: UseAutoScrollOptions): {\n scrollRef: RefObject<HTMLDivElement | null>;\n handleScroll: () => void;\n scrollOnUpdate: () => void;\n resetAutoScroll: () => void;\n};\n\ninterface UseAutoScrollOptions {\n scrollRef?: RefObject<HTMLDivElement | null>;\n threshold?: number; // Distance from bottom in px (default: 80)\n}\n```\n\n## Transport Reference\n\n### createHttpTransport\n\nCreates an HTTP/SSE transport using native `fetch()`:\n\n```typescript\nimport { createHttpTransport } from '@octavus/react';\n\nconst transport = createHttpTransport({\n request: (payload, options) =>\n fetch('/api/trigger', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, ...payload }),\n signal: options?.signal,\n }),\n});\n```\n\n### createSocketTransport\n\nCreates a WebSocket/SockJS transport for real-time connections:\n\n```typescript\nimport { createSocketTransport } from '@octavus/react';\n\nconst transport = createSocketTransport({\n connect: () =>\n new Promise((resolve, reject) => {\n const ws = new WebSocket(`wss://api.example.com/stream?sessionId=${sessionId}`);\n ws.onopen = () => resolve(ws);\n ws.onerror = () => reject(new Error('Connection failed'));\n }),\n});\n```\n\nSocket transport provides additional connection management:\n\n```typescript\n// Access connection state directly\ntransport.connectionState; // 'disconnected' | 'connecting' | 'connected' | 'error'\n\n// Subscribe to state changes\ntransport.onConnectionStateChange((state, error) => {\n /* ... */\n});\n\n// Eager connection (instead of lazy on first send)\nawait transport.connect();\n\n// Manual disconnect\ntransport.disconnect();\n```\n\nFor detailed WebSocket/SockJS usage including custom events, reconnection patterns, and server-side implementation, see [Socket Transport](/docs/client-sdk/socket-transport).\n\n## Class Reference (Framework-Agnostic)\n\n### OctavusChat\n\n```typescript\nclass OctavusChat {\n constructor(options: OctavusChatOptions);\n\n // State (read-only)\n readonly messages: UIMessage[];\n readonly status: ChatStatus; // 'idle' | 'streaming' | 'error' | 'awaiting-input'\n readonly error: OctavusError | null; // Structured error\n readonly pendingClientTools: Record<string, InteractiveTool[]>; // Interactive tools\n\n // Actions\n send(\n triggerName: string,\n input?: Record<string, unknown>,\n options?: { userMessage?: UserMessageInput },\n ): Promise<void>;\n stop(): void;\n\n // Subscription\n subscribe(callback: () => void): () => void; // Returns unsubscribe function\n}\n```\n\n## Next Steps\n\n- [HTTP Transport](/docs/client-sdk/http-transport) - HTTP/SSE integration (recommended)\n- [Socket Transport](/docs/client-sdk/socket-transport) - WebSocket and SockJS integration\n- [Messages](/docs/client-sdk/messages) - Working with message state\n- [Streaming](/docs/client-sdk/streaming) - Building streaming UIs\n- [Client Tools](/docs/client-sdk/client-tools) - Interactive browser-side tool handling\n- [Operations](/docs/client-sdk/execution-blocks) - Showing agent progress\n- [Error Handling](/docs/client-sdk/error-handling) - Handling errors with type guards\n- [File Uploads](/docs/client-sdk/file-uploads) - Uploading images and documents\n- [Examples](/docs/examples/overview) - Complete working examples\n",
|
|
843
861
|
excerpt: "Client SDK Overview Octavus provides two packages for frontend integration: | Package | Purpose | Use When | |...",
|
|
844
862
|
order: 1
|
|
845
863
|
},
|
|
@@ -1412,8 +1430,8 @@ See [Streaming Events](/docs/server-sdk/streaming#event-types) for the full list
|
|
|
1412
1430
|
section: "protocol",
|
|
1413
1431
|
title: "MCP Servers",
|
|
1414
1432
|
description: "Connecting agents to external tools via Model Context Protocol (MCP).",
|
|
1415
|
-
content: "\n# MCP Servers\n\nMCP servers extend your agent with tools from external services. Define them in your protocol, and agents automatically discover and use their tools at runtime.\n\nThere are two types of MCP servers:\n\n| Source | Description | Example |\n| -------- | ------------------------------------------------------------------ | ------------------------------ |\n| `remote` | HTTP-based MCP servers, managed by the platform | Figma, Sentry, GitHub |\n| `device` | Local MCP servers running on the consumer's machine via server-sdk | Browser automation, filesystem |\n\n## Defining MCP Servers\n\nMCP servers are defined in the `mcpServers:` section. The key becomes the **namespace** for all tools from that server.\n\n```yaml\nmcpServers:\n figma:\n description: Figma design tool integration\n source: remote\n display: description\n\n browser:\n description: Chrome DevTools browser automation\n source: device\n display: name\n```\n\n### Fields\n\n| Field | Required | Description |\n| ------------- | -------- | ------------------------------------------------------------------------------------------------------- |\n| `description` | Yes | What the MCP server provides |\n| `source` | Yes | `remote` (platform-managed) or `device` (consumer-provided) |\n| `display` | No | How tool calls appear in UI: `hidden`, `name`, `description` (default: `description`) |\n| `connection` | No | When to connect: `eager` or `lazy` (default: `lazy`). Remote only. |\n| `execution` | No | Where the MCP process runs: `sandbox` (default) or `device`. See [Device Execution](#device-execution). |\n\n### Display Modes\n\nDisplay modes control visibility of all tool calls from the MCP server, using the same modes as [regular tools](/docs/protocol/tools#display-modes):\n\n| Mode | Behavior |\n| ------------- | -------------------------------------- |\n| `hidden` | Tool calls run silently |\n| `name` | Shows tool name while executing |\n| `description` | Shows tool description while executing |\n\n## Making MCP Servers Available\n\nLike tools, MCP servers defined in `mcpServers:` must be referenced in `agent.mcpServers` to be available:\n\n```yaml\nmcpServers:\n figma:\n description: Figma design tool integration\n source: remote\n display: description\n\n sentry:\n description: Error tracking and debugging\n source: remote\n display: name\n\n browser:\n description: Chrome DevTools browser automation\n source: device\n display: name\n\n filesystem:\n description: Filesystem access for reading and writing files\n source: device\n display: hidden\n\nagent:\n model: anthropic/claude-sonnet-4-5\n system: system\n mcpServers: [figma, sentry, browser, filesystem]\n tools: [set-chat-title]\n agentic: true\n maxSteps: 100\n```\n\n## Tool Namespacing\n\nAll MCP tools are automatically namespaced using `__` (double underscore) as a separator. The namespace comes from the `mcpServers` key.\n\nFor example, a server defined as `browser:` that exposes `navigate_page` and `click` produces:\n\n- `browser__navigate_page`\n- `browser__click`\n\nA server defined as `figma:` that exposes `get_design_context` produces:\n\n- `figma__get_design_context`\n\nThe namespace is stripped before calling the MCP server - the server receives the original tool name. This convention matches Anthropic's MCP integration in Claude Desktop and ensures tool names stay unique across servers.\n\n### What the LLM Sees\n\nWhen an agent has both regular tools and MCP servers configured, the LLM sees all tools combined:\n\n```\nProtocol tools:\n set-chat-title\n\nRemote MCP tools (auto-discovered):\n figma__get_design_context\n figma__get_screenshot\n sentry__get_issues\n sentry__get_issue_details\n\nDevice MCP tools (auto-discovered):\n browser__navigate_page\n browser__click\n browser__take_snapshot\n filesystem__read_file\n filesystem__write_file\n filesystem__list_directory\n```\n\nYou don't define individual MCP tool schemas in the protocol - they're auto-discovered from each MCP server at runtime.\n\n## Remote MCP Servers\n\nRemote MCP servers (`source: remote`) connect to HTTP-based MCP endpoints. The platform manages the connection, authentication, and tool discovery.\n\nConfiguration happens in the Octavus platform UI:\n\n1. Add an MCP server to your project (URL + authentication)\n2. The server's slug must match the namespace in your protocol\n3. The platform connects, discovers tools, and makes them available to the agent\n\n### Connection Modes\n\nThe `connection` field controls when the platform connects to a remote MCP server:\n\n| Mode | Behavior |\n| ------- | ---------------------------------------------------------------------------------------------------------------------- |\n| `lazy` | (default) The agent activates integrations on demand at runtime. The agent starts responding immediately. |\n| `eager` | The platform connects and discovers tools before the first LLM request. Tools are guaranteed available from message 1. |\n\n```yaml\nmcpServers:\n sentry:\n source: remote\n connection: eager # Always connected upfront\n display: name\n\n notion:\n source: remote\n # connection defaults to lazy - agent activates when needed\n display: description\n```\n\nWith **lazy connection** (the default), the agent receives two built-in tools - one for listing available integrations and one for activating them. The agent decides which integrations it needs based on the conversation and activates them on demand. This avoids paying connection latency for integrations the agent doesn't end up using.\n\nWith **eager connection**, the platform connects to the MCP server before the first LLM request, exactly like a declared tool. Use this when the agent needs the MCP's tools from the very first message.\n\nThe `connection` field is only valid on `source: remote` - device MCPs (`source: device`) have their own connection mechanism through the server-sdk. The `connection` field is respected for remote MCPs with `execution: device` the same way as sandbox MCPs.\n\n### Authentication\n\nRemote MCP servers support multiple authentication methods:\n\n| Auth Type | Description |\n| --------- | ------------------------------- |\n| MCP OAuth | Standard MCP OAuth flow |\n| API Key | Static API key sent as a header |\n| Bearer | Bearer token authentication |\n| None | No authentication required |\n\nAuthentication is configured per-project - different projects can connect to the same MCP server with different credentials.\n\n## Device Execution\n\nThe `execution` field controls where a remote MCP server's STDIO process runs. By default (`execution: sandbox`), the process runs in the platform's sandbox. When set to `execution: device`, the STDIO process runs on the agent's computer (VM or desktop) instead.\n\n```yaml\nmcpServers:\n code-tools:\n description: Code analysis and refactoring tools\n source: remote\n execution: device # STDIO process runs on the agent's computer\n display: name\n\n sentry:\n description: Error tracking\n source: remote\n # execution defaults to sandbox - runs in the platform\n display: name\n```\n\n### When to Use\n\nUse `execution: device` when the MCP server needs access to the agent's local environment - for example, tools that read from the local filesystem, interact with running processes, or need CLIs installed on the device.\n\n### Rules\n\n- `execution` is only meaningful for `source: remote` MCPs that use STDIO transport. HTTP-transport remote MCPs always connect from the platform regardless of the `execution` setting.\n- `execution: device` is **invalid** on `source: device` MCPs (they already run on the device by definition). Using it produces a validation error.\n- The `connection` field (`eager` or `lazy`) is respected for device-executed MCPs the same way as sandbox-executed MCPs.\n\n## Device MCP Servers\n\nDevice MCP servers (`source: device`) run on the consumer's machine. The consumer provides the MCP tools via the `@octavus/computer` package (or any `ToolProvider` implementation) through the server-sdk.\n\nWhen an agent has device MCP servers:\n\n1. The consumer creates a `Computer` with matching namespaces\n2. `@octavus/computer` discovers tools from each MCP server\n3. Tool schemas are sent to the platform via the server-sdk\n4. Tool calls flow back to the consumer for execution\n\nSee [`@octavus/computer`](/docs/server-sdk/computer) for the full integration guide.\n\n### Namespace Matching\n\nThe `mcpServers` keys in the protocol must match the keys in the consumer's `Computer` configuration:\n\n```yaml\n# protocol.yaml\nmcpServers:\n browser: # \u2190 must match\n source: device\n filesystem: # \u2190 must match\n source: device\n```\n\n```typescript\nconst computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', ['--browser-url=...']),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', [dir]),\n },\n});\n```\n\nIf the consumer provides a namespace not declared in the protocol, the platform rejects it.\n\n## Thread-Level Scoping\n\nThreads can scope which MCP servers are available, the same way they scope [tools](/docs/protocol/handlers#start-thread):\n\n```yaml\nhandlers:\n user-message:\n Start research:\n block: start-thread\n thread: research\n mcpServers: [figma, browser]\n tools: [set-chat-title]\n system: research-prompt\n```\n\nThis thread can use Figma and browser tools, but not sentry or filesystem - even if those are available on the main agent.\n\n## On-Demand MCP Servers\n\nBy default, an agent can only call MCP tools whose namespace is listed in `mcpServers`. With `onDemandMcpServers`, a scope can opt into **every connected MCP of a given source** at runtime, without enumerating each one in the protocol.\n\nRemote MCPs are connected at the project level from the Octavus dashboard. Normally, each connected MCP that the agent should be able to use has to be declared in the protocol - connecting a new MCP means editing the protocol and redeploying. `onDemandMcpServers` removes that round-trip: once a source is opted in, any MCP connected to the project under that source becomes available to the agent immediately.\n\nCurrently supported for `source: remote`.\n\n### Protocol-level declaration\n\nAdd an `onDemandMcpServers:` section alongside `mcpServers:`, keyed by source. Each entry configures how the matched MCPs appear in tool lists:\n\n```yaml\nmcpServers:\n figma:\n description: Figma design tool integration\n source: remote\n display: description\n\nonDemandMcpServers:\n remote:\n description: Additional connected integrations\n display: name\n execution: device # on-demand MCPs run on the agent's computer\n contextRetention:\n toolResults: { retainLast: 5 }\n```\n\nOn-demand MCP definitions also support the `execution` field. When set, all MCPs matched by that on-demand source inherit the execution mode.\n\n### Scope-level opt-in\n\nThe agent and individual `start-thread` blocks each choose whether to pick up on-demand MCPs, by listing the sources they want:\n\n```yaml\nagent:\n mcpServers: [figma]\n onDemandMcpServers: [remote]\n\nhandlers:\n user-message:\n focused:\n block: start-thread\n mcpServers: [figma]\n # no onDemandMcpServers - this thread does NOT see on-demand MCPs\n broad:\n block: start-thread\n mcpServers: [figma]\n onDemandMcpServers: [remote]\n```\n\n### Rules\n\n- A scope's tool list includes every **connected** MCP of any referenced source, whether or not any protocol declares that slug.\n- Undeclared namespaces inherit `description`, `display`, and `contextRetention` from the per-source entry in `onDemandMcpServers`.\n- Scopes decide independently - threads do not inherit `onDemandMcpServers` from their parent, the same rule as `mcpServers:`.\n- Tool namespaces are always the connector's slug (for example `notion__search`, `linear__create_issue`). Source keys are never namespaces.\n\nWorkers opt into on-demand MCPs the same way: through `start-thread` blocks inside `steps`. A worker without a `start-thread` that lists a source won't see on-demand MCPs of that source.\n\n## Workers\n\nWorkers can declare and use MCP servers using the same `mcpServers:` syntax. Workers resolve their own MCP connections independently - they don't inherit from a parent interactive agent.\n\n```yaml\n# Worker protocol\nmcpServers:\n sentry:\n description: Error tracking and debugging\n source: remote\n display: name\n browser:\n description: Chrome DevTools browser automation\n source: device\n display: name\n\nsteps:\n Start research:\n block: start-thread\n thread: research\n model: anthropic/claude-sonnet-4-5\n system: system\n mcpServers: [sentry, browser]\n maxSteps: 10\n```\n\nSince workers don't have a global `agent:` section, MCP servers are scoped per-thread via `start-thread` - the same way tools and skills work in workers. Remote MCP connections are project-scoped, so workers in the same project share the same OAuth connections.\n\nSee [Workers](/docs/protocol/workers) for the full worker protocol reference.\n\n## Full Example\n\n```yaml\nmcpServers:\n figma:\n description: Figma design tool integration\n source: remote\n connection: eager\n display: description\n sentry:\n description: Error tracking and debugging\n source: remote\n display: name\n browser:\n description: Chrome DevTools browser automation\n source: device\n display: name\n filesystem:\n description: Filesystem access for reading and writing files\n source: device\n display: hidden\n shell:\n description: Shell command execution\n source: device\n display: name\n\ntools:\n set-chat-title:\n description: Set the title of the current chat.\n parameters:\n title: { type: string, description: The title to set }\n\nagent:\n model: anthropic/claude-opus-4-6\n system: system\n mcpServers: [figma, sentry, browser, filesystem, shell]\n tools: [set-chat-title]\n thinking: medium\n maxSteps: 300\n agentic: true\n\ntriggers:\n user-message:\n input:\n USER_MESSAGE: { type: string }\n\nhandlers:\n user-message:\n Add message:\n block: add-message\n role: user\n prompt: user-message\n input: [USER_MESSAGE]\n display: hidden\n\n Respond:\n block: next-message\n```\n\n### Cloud-Only Agent\n\nAgents that only use remote MCP servers don't need `@octavus/computer`:\n\n```yaml\nmcpServers:\n figma:\n description: Figma design tool integration\n source: remote\n connection: eager # Need design tools from message 1\n display: description\n sentry:\n description: Error tracking and debugging\n source: remote\n # Lazy (default) - agent activates when debugging is needed\n display: name\n\ntools:\n submit-code:\n description: Submit code to the user.\n parameters:\n code: { type: string }\n\nagent:\n model: anthropic/claude-sonnet-4-5\n system: system\n mcpServers: [figma, sentry]\n tools: [submit-code]\n agentic: true\n```\n",
|
|
1416
|
-
excerpt: "MCP Servers MCP servers extend your agent with tools from external services. Define them in your protocol, and agents automatically discover and use their tools at runtime. There are
|
|
1433
|
+
content: "\n# MCP Servers\n\nMCP servers extend your agent with tools from external services. Define them in your protocol, and agents automatically discover and use their tools at runtime.\n\nThere are three types of MCP servers:\n\n| Source | Description | Example |\n| ---------- | ----------------------------------------------------------------------------------- | ------------------------------------- |\n| `remote` | HTTP-based MCP servers, managed by the platform | Figma, Sentry, GitHub |\n| `device` | Local MCP servers running on the agent's machine via `@octavus/computer` | Browser automation, filesystem |\n| `consumer` | Inline MCP servers defined in your server-sdk process via `createInlineMcpServer()` | Custom integrations, third-party APIs |\n\n## Defining MCP Servers\n\nMCP servers are defined in the `mcpServers:` section. The key becomes the **namespace** for all tools from that server.\n\n```yaml\nmcpServers:\n figma:\n description: Figma design tool integration\n source: remote\n display: description\n\n browser:\n description: Chrome DevTools browser automation\n source: device\n display: name\n\n github:\n description: Repository management - issues, pull requests, code\n source: consumer\n display: name\n```\n\n### Fields\n\n| Field | Required | Description |\n| ------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |\n| `description` | Yes | What the MCP server provides |\n| `source` | Yes | `remote`, `device`, or `consumer` (see source types above) |\n| `display` | No | How tool calls appear in UI: `hidden`, `name`, `description` (default: `description`) |\n| `connection` | No | When to connect: `eager` or `lazy` (default: `lazy`). `remote` only. |\n| `execution` | No | Where the MCP process runs: `sandbox` (default) or `device`. `remote` only. See [Device Execution](#device-execution). |\n\n### Display Modes\n\nDisplay modes control visibility of all tool calls from the MCP server, using the same modes as [regular tools](/docs/protocol/tools#display-modes):\n\n| Mode | Behavior |\n| ------------- | -------------------------------------- |\n| `hidden` | Tool calls run silently |\n| `name` | Shows tool name while executing |\n| `description` | Shows tool description while executing |\n\n## Making MCP Servers Available\n\nLike tools, MCP servers defined in `mcpServers:` must be referenced in `agent.mcpServers` to be available:\n\n```yaml\nmcpServers:\n figma:\n description: Figma design tool integration\n source: remote\n display: description\n\n sentry:\n description: Error tracking and debugging\n source: remote\n display: name\n\n browser:\n description: Chrome DevTools browser automation\n source: device\n display: name\n\n filesystem:\n description: Filesystem access for reading and writing files\n source: device\n display: hidden\n\nagent:\n model: anthropic/claude-sonnet-4-5\n system: system\n mcpServers: [figma, sentry, browser, filesystem]\n tools: [set-chat-title]\n agentic: true\n maxSteps: 100\n```\n\n## Tool Namespacing\n\nAll MCP tools are automatically namespaced using `__` (double underscore) as a separator. The namespace comes from the `mcpServers` key.\n\nFor example, a server defined as `browser:` that exposes `navigate_page` and `click` produces:\n\n- `browser__navigate_page`\n- `browser__click`\n\nA server defined as `figma:` that exposes `get_design_context` produces:\n\n- `figma__get_design_context`\n\nThe namespace is stripped before calling the MCP server - the server receives the original tool name. This convention matches Anthropic's MCP integration in Claude Desktop and ensures tool names stay unique across servers.\n\n### What the LLM Sees\n\nWhen an agent has both regular tools and MCP servers configured, the LLM sees all tools combined:\n\n```\nProtocol tools:\n set-chat-title\n\nRemote MCP tools (auto-discovered):\n figma__get_design_context\n figma__get_screenshot\n sentry__get_issues\n sentry__get_issue_details\n\nDevice MCP tools (auto-discovered):\n browser__navigate_page\n browser__click\n browser__take_snapshot\n filesystem__read_file\n filesystem__write_file\n filesystem__list_directory\n\nConsumer MCP tools (provided by the server-sdk):\n github__get-pr-overview\n github__list-issues\n```\n\nYou don't define individual MCP tool schemas in the protocol - remote and device tools are auto-discovered from each MCP server at runtime, and consumer tools are supplied by the server-sdk.\n\n## Remote MCP Servers\n\nRemote MCP servers (`source: remote`) connect to HTTP-based MCP endpoints. The platform manages the connection, authentication, and tool discovery.\n\nConfiguration happens in the Octavus platform UI:\n\n1. Add an MCP server to your project (URL + authentication)\n2. The server's slug must match the namespace in your protocol\n3. The platform connects, discovers tools, and makes them available to the agent\n\n### Connection Modes\n\nThe `connection` field controls when the platform connects to a remote MCP server:\n\n| Mode | Behavior |\n| ------- | ---------------------------------------------------------------------------------------------------------------------- |\n| `lazy` | (default) The agent activates integrations on demand at runtime. The agent starts responding immediately. |\n| `eager` | The platform connects and discovers tools before the first LLM request. Tools are guaranteed available from message 1. |\n\n```yaml\nmcpServers:\n sentry:\n source: remote\n connection: eager # Always connected upfront\n display: name\n\n notion:\n source: remote\n # connection defaults to lazy - agent activates when needed\n display: description\n```\n\nWith **lazy connection** (the default), the agent receives two built-in tools - one for listing available integrations and one for activating them. The agent decides which integrations it needs based on the conversation and activates them on demand. This avoids paying connection latency for integrations the agent doesn't end up using.\n\nWith **eager connection**, the platform connects to the MCP server before the first LLM request, exactly like a declared tool. Use this when the agent needs the MCP's tools from the very first message.\n\nThe `connection` field is only valid on `source: remote` - device MCPs (`source: device`) have their own connection mechanism through the server-sdk. The `connection` field is respected for remote MCPs with `execution: device` the same way as sandbox MCPs.\n\n### Authentication\n\nRemote MCP servers support multiple authentication methods:\n\n| Auth Type | Description |\n| --------- | ------------------------------- |\n| MCP OAuth | Standard MCP OAuth flow |\n| API Key | Static API key sent as a header |\n| Bearer | Bearer token authentication |\n| None | No authentication required |\n\nAuthentication is configured per-project - different projects can connect to the same MCP server with different credentials.\n\n## Device Execution\n\nThe `execution` field controls where a remote MCP server's STDIO process runs. By default (`execution: sandbox`), the process runs in the platform's sandbox. When set to `execution: device`, the STDIO process runs on the agent's computer (VM or desktop) instead.\n\n```yaml\nmcpServers:\n code-tools:\n description: Code analysis and refactoring tools\n source: remote\n execution: device # STDIO process runs on the agent's computer\n display: name\n\n sentry:\n description: Error tracking\n source: remote\n # execution defaults to sandbox - runs in the platform\n display: name\n```\n\n### When to Use\n\nUse `execution: device` when the MCP server needs access to the agent's local environment - for example, tools that read from the local filesystem, interact with running processes, or need CLIs installed on the device.\n\n### Rules\n\n- `execution` is only meaningful for `source: remote` MCPs that use STDIO transport. HTTP-transport remote MCPs always connect from the platform regardless of the `execution` setting.\n- `execution` is **invalid** on `source: device` and `source: consumer` MCPs - they already run outside the platform. Using it produces a validation error.\n- The `connection` field (`eager` or `lazy`) is respected for device-executed MCPs the same way as sandbox-executed MCPs.\n\n## Device MCP Servers\n\nDevice MCP servers (`source: device`) run on the consumer's machine. The consumer provides the MCP tools via the `@octavus/computer` package (or any `ToolProvider` implementation) through the server-sdk.\n\nWhen an agent has device MCP servers:\n\n1. The consumer creates a `Computer` with matching namespaces\n2. `@octavus/computer` discovers tools from each MCP server\n3. Tool schemas are sent to the platform via the server-sdk\n4. Tool calls flow back to the consumer for execution\n\nSee [`@octavus/computer`](/docs/server-sdk/computer) for the full integration guide.\n\n### Namespace Matching\n\nThe `mcpServers` keys in the protocol must match the keys in the consumer's `Computer` configuration:\n\n```yaml\n# protocol.yaml\nmcpServers:\n browser: # \u2190 must match\n source: device\n filesystem: # \u2190 must match\n source: device\n```\n\n```typescript\nconst computer = new Computer({\n mcpServers: {\n browser: Computer.stdio('chrome-devtools-mcp', ['--browser-url=...']),\n filesystem: Computer.stdio('@modelcontextprotocol/server-filesystem', [dir]),\n },\n});\n```\n\nIf the consumer provides a namespace not declared in the protocol, the platform rejects it.\n\n## Consumer MCP Servers\n\nConsumer MCP servers (`source: consumer`) are defined inline in your server-sdk process. Tool schemas and handlers live in your code; the platform learns the namespace from the protocol and routes tool calls to your process via the same `dynamicToolSchemas` channel that device MCPs use.\n\n```yaml\nmcpServers:\n github:\n description: Repository management - issues, pull requests, code\n source: consumer\n display: name\n\nagent:\n mcpServers:\n - github\n```\n\nThe protocol declaration is intentionally minimal - the SDK supplies tool names and JSON schemas at runtime, so adding or evolving tools doesn't require a protocol change.\n\nUse consumer MCPs when:\n\n- The integration's credentials should never reach the platform (OAuth tokens, customer API keys).\n- You want to group an integration's tools (`github__list-prs`, `github__get-issue`) without enumerating each one in YAML.\n- You want type-safe handler arguments via Zod schemas.\n\nSee [`createInlineMcpServer`](/docs/server-sdk/inline-mcp) in the server-sdk reference for the full implementation guide.\n\n### Namespace Matching\n\nThe protocol namespace must match the namespace passed to `createInlineMcpServer()`:\n\n```yaml\n# protocol.yaml\nmcpServers:\n github: # \u2190 must match\n source: consumer\n```\n\n```typescript\nconst github = createInlineMcpServer('github', {\n /* tools... */\n});\n\nsession = client.agentSessions.attach(sessionId, {\n mcpServers: [github],\n});\n```\n\nIf the SDK provides a namespace not declared in the protocol, those tools are filtered out at the runtime boundary and the LLM never sees them.\n\n## Thread-Level Scoping\n\nThreads can scope which MCP servers are available, the same way they scope [tools](/docs/protocol/handlers#start-thread):\n\n```yaml\nhandlers:\n user-message:\n Start research:\n block: start-thread\n thread: research\n mcpServers: [figma, browser]\n tools: [set-chat-title]\n system: research-prompt\n```\n\nThis thread can use Figma and browser tools, but not sentry or filesystem - even if those are available on the main agent.\n\n## On-Demand MCP Servers\n\nBy default, an agent can only call MCP tools whose namespace is listed in `mcpServers`. With `onDemandMcpServers`, a scope can opt into **every connected MCP of a given source** at runtime, without enumerating each one in the protocol.\n\nRemote MCPs are connected at the project level from the Octavus dashboard. Normally, each connected MCP that the agent should be able to use has to be declared in the protocol - connecting a new MCP means editing the protocol and redeploying. `onDemandMcpServers` removes that round-trip: once a source is opted in, any MCP connected to the project under that source becomes available to the agent immediately.\n\nCurrently supported for `source: remote`.\n\n### Protocol-level declaration\n\nAdd an `onDemandMcpServers:` section alongside `mcpServers:`, keyed by source. Each entry configures how the matched MCPs appear in tool lists:\n\n```yaml\nmcpServers:\n figma:\n description: Figma design tool integration\n source: remote\n display: description\n\nonDemandMcpServers:\n remote:\n description: Additional connected integrations\n display: name\n execution: device # on-demand MCPs run on the agent's computer\n contextRetention:\n toolResults: { retainLast: 5 }\n```\n\nOn-demand MCP definitions also support the `execution` field. When set, all MCPs matched by that on-demand source inherit the execution mode.\n\n### Scope-level opt-in\n\nThe agent and individual `start-thread` blocks each choose whether to pick up on-demand MCPs, by listing the sources they want:\n\n```yaml\nagent:\n mcpServers: [figma]\n onDemandMcpServers: [remote]\n\nhandlers:\n user-message:\n focused:\n block: start-thread\n mcpServers: [figma]\n # no onDemandMcpServers - this thread does NOT see on-demand MCPs\n broad:\n block: start-thread\n mcpServers: [figma]\n onDemandMcpServers: [remote]\n```\n\n### Rules\n\n- A scope's tool list includes every **connected** MCP of any referenced source, whether or not any protocol declares that slug.\n- Undeclared namespaces inherit `description`, `display`, and `contextRetention` from the per-source entry in `onDemandMcpServers`.\n- Scopes decide independently - threads do not inherit `onDemandMcpServers` from their parent, the same rule as `mcpServers:`.\n- Tool namespaces are always the connector's slug (for example `notion__search`, `linear__create_issue`). Source keys are never namespaces.\n\nWorkers opt into on-demand MCPs the same way: through `start-thread` blocks inside `steps`. A worker without a `start-thread` that lists a source won't see on-demand MCPs of that source.\n\n## Workers\n\nWorkers can declare and use MCP servers using the same `mcpServers:` syntax. Workers resolve their own MCP connections independently - they don't inherit from a parent interactive agent.\n\n```yaml\n# Worker protocol\nmcpServers:\n sentry:\n description: Error tracking and debugging\n source: remote\n display: name\n browser:\n description: Chrome DevTools browser automation\n source: device\n display: name\n\nsteps:\n Start research:\n block: start-thread\n thread: research\n model: anthropic/claude-sonnet-4-5\n system: system\n mcpServers: [sentry, browser]\n maxSteps: 10\n```\n\nSince workers don't have a global `agent:` section, MCP servers are scoped per-thread via `start-thread` - the same way tools and skills work in workers. Remote MCP connections are project-scoped, so workers in the same project share the same OAuth connections.\n\nSee [Workers](/docs/protocol/workers) for the full worker protocol reference.\n\n## Full Example\n\n```yaml\nmcpServers:\n figma:\n description: Figma design tool integration\n source: remote\n connection: eager\n display: description\n sentry:\n description: Error tracking and debugging\n source: remote\n display: name\n browser:\n description: Chrome DevTools browser automation\n source: device\n display: name\n filesystem:\n description: Filesystem access for reading and writing files\n source: device\n display: hidden\n shell:\n description: Shell command execution\n source: device\n display: name\n\ntools:\n set-chat-title:\n description: Set the title of the current chat.\n parameters:\n title: { type: string, description: The title to set }\n\nagent:\n model: anthropic/claude-opus-4-6\n system: system\n mcpServers: [figma, sentry, browser, filesystem, shell]\n tools: [set-chat-title]\n thinking: medium\n maxSteps: 300\n agentic: true\n\ntriggers:\n user-message:\n input:\n USER_MESSAGE: { type: string }\n\nhandlers:\n user-message:\n Add message:\n block: add-message\n role: user\n prompt: user-message\n input: [USER_MESSAGE]\n display: hidden\n\n Respond:\n block: next-message\n```\n\n### Cloud-Only Agent\n\nAgents that only use remote MCP servers don't need `@octavus/computer`:\n\n```yaml\nmcpServers:\n figma:\n description: Figma design tool integration\n source: remote\n connection: eager # Need design tools from message 1\n display: description\n sentry:\n description: Error tracking and debugging\n source: remote\n # Lazy (default) - agent activates when debugging is needed\n display: name\n\ntools:\n submit-code:\n description: Submit code to the user.\n parameters:\n code: { type: string }\n\nagent:\n model: anthropic/claude-sonnet-4-5\n system: system\n mcpServers: [figma, sentry]\n tools: [submit-code]\n agentic: true\n```\n",
|
|
1434
|
+
excerpt: "MCP Servers MCP servers extend your agent with tools from external services. Define them in your protocol, and agents automatically discover and use their tools at runtime. There are three types of...",
|
|
1417
1435
|
order: 13
|
|
1418
1436
|
}
|
|
1419
1437
|
]
|
|
@@ -1520,4 +1538,4 @@ export {
|
|
|
1520
1538
|
getDocSlugs,
|
|
1521
1539
|
getSectionBySlug
|
|
1522
1540
|
};
|
|
1523
|
-
//# sourceMappingURL=chunk-
|
|
1541
|
+
//# sourceMappingURL=chunk-VGQQ3XM5.js.map
|