diff --git a/README.md b/README.md index 3ebb744..072bb88 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,13 @@ Documentation is available at https://kiro.dev/docs/powers/ --- +### mcp-agentic +**MCP Agentic** - Connect MCP clients to ACP-compatible agents through a local MCP bridge built on stdio Bus with embedded runtime. Supports in-process agents, external worker processes, session management, and one-shot delegation. + +**MCP Servers:** mcp-agentic + +--- + ### postman **API Testing with Postman** - Automate API testing and collection management with Postman - create workspaces, collections, environments, and run tests programmatically. diff --git a/mcp-agentic/POWER.md b/mcp-agentic/POWER.md new file mode 100644 index 0000000..2b89210 --- /dev/null +++ b/mcp-agentic/POWER.md @@ -0,0 +1,469 @@ +--- +name: "mcp-agentic" +displayName: "MCP Agentic" +description: "Connect MCP clients to ACP-compatible agents through a local MCP bridge built on stdio Bus with embedded runtime and multi-provider AI support (OpenAI, Anthropic, Google Gemini)" +keywords: + - acp + - agent client protocol + - mcp + - stdio bus + - delegation + - multi-agent + - codex acp + - external agent + - agent discovery + - session routing + - continue session + - route task to agent + - embedded runtime + - worker orchestration + - multi-provider + - openai + - anthropic + - gemini + - runtime-params + - provider-discovery +author: "stdio Bus" +license: "Apache-2.0" +--- + +# MCP Agentic + +This power enables MCP clients to communicate with ACP-compatible agents through a local MCP bridge. Agents can run in-process (via `AgentHandler` implementations) or as external worker processes (via `@stdiobus/node` StdioBus). The single entry point is `McpAgenticServer`, which owns the MCP server, tool registration, and executor lifecycle. + +The power includes a **multi-provider AI layer** supporting OpenAI, Anthropic, and Google Gemini through their native SDKs. The **Factory API** (`openAI()`, `anthropic()`, `gemini()`, `createMultiProviderAgent()`) is the recommended way to configure providers with flat, typed options and Zod validation. Custom providers can be created via `defineProvider()`. Providers expose `kind` and `capabilities` metadata for enriched discovery via `agents_discover`. + +## Use this power when + +- **Discovering available agents** — find registered agents and their capabilities +- **Delegating work to external agents** — route tasks to specialized agents +- **Managing agent sessions** — create, prompt, check status, cancel, or close sessions +- **Multi-step delegated work** — preserve session continuity across multiple interactions +- **One-shot delegation** — delegate a task in a single call (create + prompt + close) +- **Selecting AI providers** — choose between OpenAI, Anthropic, or Google Gemini per session +- **Tuning AI parameters at runtime** — override model, temperature, systemPrompt, and other parameters per request + +Do not use this power when the task can be completed fully without external delegation. + +## Runtime model + +> **Note:** The `mcp.json` config shipped with this power starts the default CLI reference server, which has no agents registered. It is useful for verifying MCP connectivity and inspecting the tool schema, but cannot delegate work. For actual agent delegation, create your own server script that calls `server.register()` before `server.start()` — see the Programmatic setup example below. + +**Architecture:** +- The MCP client communicates with 8 MCP tools exposed by `McpAgenticServer` +- `McpAgenticServer` routes tool calls to an `AgentExecutor` backend +- Two executor backends: + - **InProcessExecutor** — calls `AgentHandler` instances directly in-memory + - **WorkerExecutor** — routes requests through `@stdiobus/node` StdioBus to ACP worker processes +- In-process agents take priority over workers when an agent ID exists in both +- Sessions are tracked per-executor with TTL and idle expiry +- **Provider Layer** — an extensible layer of AI providers (`src/provider/`) that normalizes requests and responses across different AI services through the `AIProvider` interface + +**Key components:** +- **McpAgenticServer** — single public entry point; owns MCP server and executor lifecycle +- **AgentHandler** — public interface users implement for custom agent logic +- **AgentExecutor** — internal interface abstracting execution backends +- **InProcessExecutor** — direct in-memory agent calls, session management, lifecycle hooks +- **WorkerExecutor** — StdioBus transport to external ACP worker processes +- **Tool Handlers** — decoupled functions in `src/mcp/tools/*.ts` that depend only on `AgentExecutor` +- **ProviderRegistry** — registry of AI providers; supports registration, lookup, and discovery of available providers and their models +- **MultiProviderCompanionAgent** — agent implementing `AgentHandler` that delegates AI generation to any registered provider, with dynamic provider selection per session and runtime parameter overrides + +## MCP tools (8 total) + +| Tool | Description | +|------|-------------| +| `bridge_health` | Check bridge readiness | +| `agents_discover` | List available agents, optionally filter by capability. Response includes a `providers` field for agents that support multiple AI providers, listing each provider's `id`, `models`, `kind`, `capabilities`, `displayName`, and `description`. | +| `sessions_create` | Create a new agent session, returns a `sessionId`. Accepts `metadata.provider` to select a specific AI provider for the session, and `metadata.runtimeParams` for session-level parameter defaults. | +| `sessions_prompt` | Send a prompt to an existing session. Accepts an optional `runtimeParams` field to override provider parameters (model, temperature, systemPrompt, etc.) for this specific prompt. | +| `sessions_status` | Check the status of an existing session | +| `sessions_close` | Close a session when done | +| `sessions_cancel` | Cancel an in-flight prompt request | +| `tasks_delegate` | One-shot delegation (create session + prompt + close). Accepts an optional `runtimeParams` field to override provider parameters for this delegation. | + +## Mandatory rules + +- **Never invent agents, capabilities, or statuses** — always use actual discovery results +- **Never claim delegation succeeded unless the bridge confirms it** — wait for explicit confirmation +- **Never silently switch sessions** — report session changes explicitly +- **Preserve structured tool outputs exactly** — do not transform or summarize results +- **Surface failures explicitly** — report errors with full context and `BridgeError` category +- **Prefer `tasks_delegate` for one-shot work** — use `sessions_*` tools only for multi-turn conversations + +## Preferred tool sequence + +1. **Check bridge readiness** — `bridge_health` +2. **Discover agents** — `agents_discover` (filter by capability if needed) +3. **Create a session** — `sessions_create` with `agentId` (and optionally `metadata.provider` to select a provider) +4. **Submit the task** — `sessions_prompt` with `sessionId` and `prompt` (and optionally `runtimeParams`) +5. **Check status** — `sessions_status` if the task is long-running +6. **Close session** — `sessions_close` when work is complete +7. **Cancel if needed** — `sessions_cancel` to abort an in-flight prompt + +For one-shot tasks, use `tasks_delegate` instead of steps 3–6. + +## Output expectations + +**Successful delegation returns:** +- `sessionId` — session identifier for continuity +- `agentId` — identifier of the agent handling the request +- `status` — current session status (`active`, `idle`, `busy`, `closed`, `failed`) +- `text` — agent response text (from prompt) +- `stopReason` — why the agent stopped (`end_turn`, `max_tokens`, `content_filter`, `cancelled`) +- `usage` — token usage statistics (when available): `{ inputTokens, outputTokens }` + +**Agent discovery (`agents_discover`) returns:** +- `id` — agent identifier +- `capabilities` — list of agent capabilities +- `status` — agent status (`ready`, `busy`, `unavailable`) +- `providers` — (optional) array of available AI providers when the agent supports multiple providers, each with `id`, `models`, and enriched metadata (`kind`, `capabilities`, `displayName`, `description`) + +**`sessions_prompt` accepts:** +- `sessionId` — target session +- `prompt` — prompt text +- `timeout` — optional request timeout in milliseconds +- `runtimeParams` — optional runtime parameter overrides (see RuntimeParams below) + +**`tasks_delegate` accepts:** +- `prompt` — prompt text +- `agentId` — optional target agent +- `timeout` — optional request timeout in milliseconds +- `metadata` — optional session metadata (including `provider` for provider selection) +- `runtimeParams` — optional runtime parameter overrides + +**Failures return a `BridgeError` with:** +- `type` — error category (`CONFIG`, `AUTH`, `UPSTREAM`, `TRANSPORT`, `TIMEOUT`, `INTERNAL`) +- `message` — human-readable error description +- `details.retryable` — whether the operation can be retried +- `details.sessionValid` — whether the session remains valid after failure + +## Configuration + +`McpAgenticServer` accepts a `McpAgenticServerConfig` object: + +```typescript +interface McpAgenticServerConfig { + /** Pre-register in-process agents at construction time. */ + agents?: AgentHandler[]; + /** Default agent ID when none is specified in session creation. */ + defaultAgentId?: string; + /** Maximum concurrent in-flight tool requests. Default: 50. */ + maxConcurrentRequests?: number; + /** Maximum prompt size in bytes. Default: 1048576 (1 MiB). */ + maxPromptBytes?: number; + /** Maximum metadata size in bytes (JSON-serialized). Default: 65536 (64 KiB). */ + maxMetadataBytes?: number; +} +``` + +### MultiProviderCompanionConfig + +Configuration for constructing a `MultiProviderCompanionAgent`: + +```typescript +interface MultiProviderCompanionConfig { + /** Unique agent identifier. */ + id: string; + /** Default provider id to use when no override is specified. */ + defaultProviderId: string; + /** Registry of available AI providers. */ + registry: ProviderRegistry; + /** Optional list of capabilities this agent supports. */ + capabilities?: string[]; + /** Default system prompt applied to all sessions unless overridden. */ + systemPrompt?: string; + /** Provider-level default RuntimeParams. */ + defaults?: RuntimeParams; +} +``` + +### ProviderConfig + +Configuration for constructing a provider instance: + +```typescript +interface ProviderConfig { + /** Credential key-value pairs (e.g., { apiKey: '...' }). Sourced from env by the caller. */ + credentials: Record; + /** Model identifiers available for this provider. */ + models: string[]; + /** Default RuntimeParams applied when no override is specified. */ + defaults?: RuntimeParams; +} +``` + +### RuntimeParams + +Parameters for AI generation, passed dynamically at runtime: + +```typescript +interface RuntimeParams { + model?: string; // Model identifier + temperature?: number; // Sampling temperature (0–2) + maxTokens?: number; // Maximum tokens to generate + topP?: number; // Nucleus sampling (0–1) + topK?: number; // Top-K sampling + stopSequences?: string[]; // Stop sequences + systemPrompt?: string; // System prompt override + providerSpecific?: Record; // Provider-native parameters +} +``` + +### Programmatic setup + +```typescript +import { McpAgenticServer } from '@stdiobus/mcp-agentic'; + +const server = new McpAgenticServer({ defaultAgentId: 'my-agent' }) + .register({ + id: 'my-agent', + capabilities: ['code-analysis'], + async prompt(sessionId, input) { + return { text: `Analyzed: ${input}`, stopReason: 'end_turn' }; + }, + }) + .registerWorker({ + id: 'py-agent', + command: 'python', + args: ['agent.py'], + capabilities: ['data-analysis'], + }); + +await server.start(); +``` + +### Worker configuration + +Workers are registered via `registerWorker()` with a `WorkerConfig`: + +```typescript +interface WorkerConfig { + id: string; + command: string; + args: string[]; + env?: Record; + capabilities?: string[]; +} +``` + +## Multi-provider configuration + +The multi-provider layer allows using OpenAI, Anthropic, and Google Gemini through their native SDKs with a unified interface. + +### Peer dependencies + +Install only the provider SDKs you need: + +```bash +# OpenAI +npm install openai + +# Anthropic +npm install @anthropic-ai/sdk + +# Google Gemini +npm install @google/generative-ai +``` + +### ProviderConfig and RuntimeParams + +Each provider is constructed with a `ProviderConfig` containing credentials (sourced from environment variables by the caller), a list of available models, and optional default `RuntimeParams`. Providers never access `process.env` directly after construction. + +### Three-level merge priority + +RuntimeParams are merged in ascending priority: + +``` +ProviderConfig.defaults < session metadata.runtimeParams < prompt-level runtimeParams +``` + +- Only defined (non-`undefined`) fields from higher-priority layers override lower ones. +- `providerSpecific` is shallow-merged (spread) across all layers, not replaced. + +### Programmatic setup with providers + +```typescript +import { + McpAgenticServer, + openAI, + anthropic, + gemini, + createMultiProviderAgent, +} from '@stdiobus/mcp-agentic'; + +// Create multi-provider agent using Factory API +const agent = createMultiProviderAgent({ + id: 'multi-ai', + defaultProviderId: 'openai', + providers: [ + openAI({ + apiKey: process.env.OPENAI_API_KEY!, + models: ['gpt-4o', 'gpt-4o-mini'], + defaults: { temperature: 0.7 }, + }), + anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY!, + models: ['claude-sonnet-4-20250514'], + }), + gemini({ + apiKey: process.env.GOOGLE_AI_API_KEY!, + models: ['gemini-2.0-flash'], + }), + ], + capabilities: ['chat', 'analysis'], + systemPrompt: 'You are a helpful assistant.', +}); + +// Register and start server +const server = new McpAgenticServer({ defaultAgentId: 'multi-ai' }) + .register(agent); + +await server.start(); +``` + +## Steering references + +Load the relevant guidance depending on the task: + +- `steering/activation-and-scope.md` — when to use this power +- `steering/discovery-and-routing.md` — agent discovery and routing rules +- `steering/delegation-and-session-lifecycle.md` — session management patterns +- `steering/failure-handling.md` — error handling strategies +- `steering/configuration.md` — configuration options and best practices + +## Examples + +**Discover available agents and their providers:** +``` +agents_discover({ capability: "chat" }) +→ [{ + id: "multi-ai", + capabilities: ["chat", "analysis"], + status: "ready", + providers: [ + { id: "openai", models: ["gpt-4o", "gpt-4o-mini"], kind: "llm", capabilities: { streaming: true, tools: true, vision: true, jsonMode: true }, displayName: "OpenAI" }, + { id: "anthropic", models: ["claude-sonnet-4-20250514"], kind: "llm", capabilities: { streaming: true, tools: true, vision: true, jsonMode: false }, displayName: "Anthropic" }, + { id: "google-gemini", models: ["gemini-2.0-flash"], kind: "llm", capabilities: { streaming: false, tools: false, vision: true, jsonMode: true }, displayName: "Google Gemini" } + ] + }] +``` + +**Create session with a specific provider:** +``` +sessions_create({ agentId: "multi-ai", metadata: { provider: "anthropic" } }) +→ { sessionId: "abc-123", agentId: "multi-ai", status: "active" } +``` + +**Prompt with runtimeParams override:** +``` +sessions_prompt({ + sessionId: "abc-123", + prompt: "Explain quantum computing", + runtimeParams: { temperature: 0.3, model: "claude-sonnet-4-20250514", systemPrompt: "Explain concepts simply." } +}) +→ { text: "Quantum computing uses...", stopReason: "end_turn", usage: { inputTokens: 42, outputTokens: 128 } } +``` + +**One-shot delegation with runtimeParams:** +``` +tasks_delegate({ + agentId: "multi-ai", + prompt: "Summarize this document", + metadata: { provider: "openai" }, + runtimeParams: { temperature: 0, maxTokens: 200 } +}) +→ { sessionId: "def-456", text: "The document covers...", stopReason: "end_turn" } +``` + +**Multi-provider switching in one server:** +``` +// Session 1: Use OpenAI +sessions_create({ agentId: "multi-ai", metadata: { provider: "openai" } }) +→ { sessionId: "s1", agentId: "multi-ai", status: "active" } + +sessions_prompt({ sessionId: "s1", prompt: "Hello" }) +→ { text: "Hi there! (from OpenAI)", stopReason: "end_turn" } + +// Session 2: Use Anthropic +sessions_create({ agentId: "multi-ai", metadata: { provider: "anthropic" } }) +→ { sessionId: "s2", agentId: "multi-ai", status: "active" } + +sessions_prompt({ sessionId: "s2", prompt: "Hello" }) +→ { text: "Hello! (from Anthropic)", stopReason: "end_turn" } +``` + +**Delegate a task (one-shot, basic):** +``` +tasks_delegate({ agentId: "my-agent", prompt: "Analyze this codebase for security issues" }) +→ { sessionId: "abc-123", text: "Found 3 issues...", stopReason: "end_turn" } +``` + +**Multi-turn session (basic):** +``` +sessions_create({ agentId: "my-agent" }) +→ { sessionId: "abc-123", agentId: "my-agent", status: "active" } + +sessions_prompt({ sessionId: "abc-123", prompt: "Analyze this codebase" }) +→ { text: "Found 3 issues...", stopReason: "end_turn" } + +sessions_prompt({ sessionId: "abc-123", prompt: "Now suggest fixes" }) +→ { text: "Here are the fixes...", stopReason: "end_turn" } + +sessions_close({ sessionId: "abc-123" }) +→ { success: true } +``` + +## Troubleshooting + +**Bridge not starting:** +- Check that Node.js >= 20.0.0 is installed +- Verify `@stdiobus/node` is available (for worker mode) +- Review bridge logs in stderr + +**Agent discovery returns empty:** +- The default CLI (`npx @stdiobus/mcp-agentic`) starts with no agents registered — this is expected. Create a custom entry point that calls `server.register()` before `server.start()`. +- If using a custom entry point, ensure agents are registered via `register()` or `registerWorker()` before starting the server. +- Check agent status — agents may be `unavailable` + +**Provider SDK not installed:** +- Provider SDKs (`openai`, `@anthropic-ai/sdk`, `@google/generative-ai`) are peer dependencies — install only the ones you need +- If you see a module-not-found error for a provider SDK, run `npm install ` + +**API key missing → BridgeError CONFIG:** +- Each provider validates that required credentials (e.g., `apiKey`) are present and non-empty at construction time +- If a credential is missing, the provider throws a `BridgeError` with category `CONFIG` specifying which credential is absent +- Ensure environment variables are set before constructing providers + +**Invalid API key → BridgeError AUTH:** +- If the AI service rejects the API key at request time, the provider throws a `BridgeError` with category `AUTH` +- Verify the API key is valid and has the required permissions + +**Rate limiting → BridgeError UPSTREAM (retryable):** +- When a provider receives a rate-limit response (HTTP 429), it throws a `BridgeError` with category `UPSTREAM` and `retryable: true` +- Wait and retry the request + +**Unknown provider in metadata → BridgeError CONFIG:** +- If `metadata.provider` in `sessions_create` specifies a provider id not registered in the `ProviderRegistry`, a `BridgeError` with category `CONFIG` is thrown +- Use `agents_discover` to check available providers before creating a session + +**Session errors:** +- Verify session IDs are preserved across calls +- Check session TTL and idle expiry settings +- Sessions expire after configurable TTL (default: 1 hour) or idle timeout (default: 10 minutes) + +**Backpressure errors:** +- `Server overloaded` means `maxConcurrentRequests` limit is reached +- This error is retryable — wait and retry + +**Input size errors:** +- `Prompt exceeds maximum size` — reduce prompt size or increase `maxPromptBytes` +- `Metadata exceeds maximum size` — reduce metadata or increase `maxMetadataBytes` + +## Security + +- **Input validation** — prompt and metadata sizes are validated before forwarding +- **Session isolation** — sessions are isolated per executor +- **Backpressure** — concurrent request limiting prevents resource exhaustion +- **Credential handling** — never hardcode credentials; use environment variables for worker processes via `WorkerConfig.env` +- **Provider credentials** — providers accept credentials via `ProviderConfig.credentials` at construction time and never access `process.env` directly after construction diff --git a/mcp-agentic/mcp.json b/mcp-agentic/mcp.json new file mode 100644 index 0000000..d98a375 --- /dev/null +++ b/mcp-agentic/mcp.json @@ -0,0 +1,22 @@ +{ + "mcpServers": { + "mcp-agentic": { + "command": "npx", + "args": [ + "-y", + "@stdiobus/mcp-agentic" + ], + "env": { + "NODE_ENV": "production", + "LOG_LEVEL": "info" + }, + "disabled": false, + "autoApprove": [ + "bridge_health", + "agents_discover", + "sessions_status" + ], + "disabledTools": [] + } + } +} diff --git a/mcp-agentic/steering/activation-and-scope.md b/mcp-agentic/steering/activation-and-scope.md new file mode 100644 index 0000000..6a4ceb3 --- /dev/null +++ b/mcp-agentic/steering/activation-and-scope.md @@ -0,0 +1,85 @@ +# Activation and Scope + +## When to use this power + +Use the MCP Agentic power **only** when external agent delegation is actually required. + +### Activate for: + +- **External agent execution** — tasks that require specialized agents (in-process or worker-based) +- **Agent discovery** — finding available agents and their capabilities via `agents_discover` +- **Provider selection** — choosing the right AI provider (OpenAI, Anthropic, Gemini) for a task based on model availability or capabilities +- **Runtime parameter tuning** — dynamically adjusting temperature, model, systemPrompt, maxTokens, and other generation parameters per request via `runtimeParams` +- **Session-based delegation** — multi-step work that needs session continuity via `sessions_*` tools +- **One-shot delegation** — single tasks via `tasks_delegate` +- **Structured result collection** — retrieving formatted outputs from agents +- **Health checks** — verifying bridge readiness via `bridge_health` + +### Do NOT activate for: + +- **Purely local editing** — file modifications that the MCP client can handle directly +- **Direct reasoning** — analysis or planning that the MCP client can complete itself +- **Speculative orchestration** — multi-agent setups without a concrete task +- **Simple queries** — questions that don't require external agent capabilities +- **Configuration changes** — modifications to local settings or files + +## Session continuity check + +Before opening a new session, **always check** whether the user is continuing an existing delegated task: + +1. **Review conversation history** — look for previous delegation operations +2. **Check for session references** — identify any active `sessionId` values +3. **Assess user intent** — determine if this is follow-up work +4. **Use existing session** — call `sessions_prompt` with the existing `sessionId` when continuity is intended + +### Indicators of session continuity: + +- User says "continue", "also", "now", "next", "then" +- Task is clearly related to previous delegation +- Same agent is referenced +- User expects context from previous interaction + +### When to open a new session: + +- User explicitly requests a new task +- Different agent or capability is needed +- Previous session was explicitly closed +- Task is unrelated to previous work +- Previous session has expired (TTL or idle timeout) + +## Scope boundaries + +This power handles **agent delegation and session management**. Specifically, it: + +- Validates tool inputs via Zod schemas (prompt size, metadata size, required fields) +- Manages session lifecycle (create, prompt, status, close, cancel) +- Routes requests to the correct executor (in-process or worker) +- Enforces backpressure and input size limits +- Maps errors to MCP-compatible responses + +It does NOT: + +- Interpret prompt content or make decisions based on what the user asked +- Run inference, heuristics, or AI logic (that's the agent's job) +- Transform agent responses — results are passed through unchanged +- Implement ACP protocol logic — workers handle their own protocol + +## Validation before activation + +Before using this power, verify: + +1. **Task requires external delegation** — cannot be completed by the MCP client alone +2. **Agent exists** — target capability is available (use `agents_discover`) +3. **Provider is available** — if a specific AI provider is needed, check the `providers` field in the `agents_discover` response to confirm the provider and desired model are registered +4. **Bridge is healthy** — use `bridge_health` if uncertain +5. **User intent is clear** — task requirements are well-defined + +## Deactivation criteria + +Stop using this power when: + +- Task is complete and no follow-up is expected +- User explicitly requests to stop delegation +- All sessions have been closed +- Bridge becomes unavailable +- Task can be completed locally without delegation diff --git a/mcp-agentic/steering/configuration.md b/mcp-agentic/steering/configuration.md new file mode 100644 index 0000000..d2b8539 --- /dev/null +++ b/mcp-agentic/steering/configuration.md @@ -0,0 +1,257 @@ +# Configuration + +## McpAgenticServerConfig + +`McpAgenticServer` is configured via a `McpAgenticServerConfig` object passed to the constructor. There is no external config file or `AGENT_CONFIG_PATH` environment variable. + +```typescript +interface McpAgenticServerConfig { + agents?: AgentHandler[]; // Pre-register in-process agents + defaultAgentId?: string; // Default agent when none specified + maxConcurrentRequests?: number; // Backpressure limit (default: 50) + maxPromptBytes?: number; // Max prompt size (default: 1 MiB) + maxMetadataBytes?: number; // Max metadata size (default: 64 KiB) + silent?: boolean; // Suppress executor stderr logging (default: false) +} +``` + +## Quick start with Factory API (recommended) + +The Factory API is the recommended way to create providers and multi-provider agents: + +```typescript +import { + McpAgenticServer, + openAI, + anthropic, + gemini, + createMultiProviderAgent, +} from '@stdiobus/mcp-agentic'; + +const agent = createMultiProviderAgent({ + id: 'companion', + defaultProviderId: 'openai', + providers: [ + openAI({ apiKey: process.env.OPENAI_API_KEY ?? '', models: ['gpt-4o'] }), + anthropic({ apiKey: process.env.ANTHROPIC_API_KEY ?? '', models: ['claude-sonnet-4-20250514'] }), + gemini({ apiKey: process.env.GOOGLE_AI_API_KEY ?? '', models: ['gemini-2.0-flash'] }), + ], + capabilities: ['general'], + systemPrompt: 'You are a helpful assistant.', + defaults: { temperature: 0.7 }, +}); + +const server = new McpAgenticServer({ defaultAgentId: 'companion' }) + .register(agent); + +await server.start(); +``` + +### Factory options + +Each factory accepts flat, typed options with Zod validation at call time: + +| Factory | Options | Required | Optional | +|---------|---------|----------|----------| +| `openAI()` | `OpenAIOptions` | `apiKey: string`, `models: string[]` | `defaults?: RuntimeParams` | +| `anthropic()` | `AnthropicOptions` | `apiKey: string`, `models: string[]` | `defaults?: RuntimeParams` | +| `gemini()` | `GeminiOptions` | `apiKey: string`, `models: string[]` | `defaults?: RuntimeParams` | + +Invalid options (empty `apiKey`, empty `models`) throw `BridgeError.config` immediately. + +If the provider SDK is not installed, the factory throws `BridgeError.config` with an installation instruction. + +### createMultiProviderAgent + +`createMultiProviderAgent()` replaces manual `ProviderRegistry` + `MultiProviderCompanionAgent` wiring: + +```typescript +interface CreateMultiProviderAgentConfig { + id: string; // Unique agent identifier + providers: AIProvider[]; // Array of provider instances + defaultProviderId: string; // Must match one provider's id + capabilities?: string[]; // Agent capabilities for discovery + systemPrompt?: string; // Default system prompt + defaults?: RuntimeParams; // Agent-level default parameters +} +``` + +Validation: empty `providers`, duplicate provider ids, or unknown `defaultProviderId` throw `BridgeError.config`. + +### Custom providers with defineProvider + +Use `defineProvider()` to create custom providers with Zod validation and discoverable metadata: + +```typescript +import { defineProvider } from '@stdiobus/mcp-agentic'; +import { z } from 'zod'; + +const myProvider = defineProvider({ + id: 'my-llm', + kind: 'llm', + displayName: 'My LLM', + description: 'Custom LLM integration', + capabilities: { streaming: true, tools: false, vision: false, jsonMode: false }, + schema: z.object({ + apiKey: z.string().min(1), + models: z.array(z.string()).nonempty(), + }), + create: (options) => ({ + id: 'my-llm', + models: options.models, + async complete(messages, params) { + // Your implementation here + return { text: '...', stopReason: 'end_turn' }; + }, + }), +}); + +// Use alongside built-in factories +const agent = createMultiProviderAgent({ + id: 'companion', + defaultProviderId: 'openai', + providers: [ + openAI({ apiKey: '...', models: ['gpt-4o'] }), + myProvider({ apiKey: '...', models: ['my-model'] }), + ], +}); +``` + +Static metadata (`factory.id`, `factory.kind`, `factory.schema`, `factory.capabilities`) is accessible without calling the factory. `agents_discover` automatically includes `displayName`, `description`, and `capabilities` for custom providers. + +## In-process agents + +Register agents programmatically using the fluent API: + +```typescript +import { McpAgenticServer } from '@stdiobus/mcp-agentic'; + +const server = new McpAgenticServer({ defaultAgentId: 'my-agent' }) + .register({ + id: 'my-agent', + capabilities: ['code-analysis', 'debugging'], + async prompt(sessionId, input) { + return { text: `Response: ${input}`, stopReason: 'end_turn' }; + }, + }); +``` + +### AgentHandler interface + +Agents implement the `AgentHandler` interface: + +```typescript +interface AgentHandler { + readonly id: string; + readonly capabilities?: string[]; + prompt?(sessionId: string, input: string, opts?: PromptOpts): Promise; + stream?(sessionId: string, input: string, opts?: StreamOpts): AsyncIterable; + onSessionCreate?(sessionId: string, metadata?: Record): Promise; + onSessionClose?(sessionId: string, reason?: string): Promise; + cancel?(sessionId: string, requestId?: string): Promise; +} +``` + +At minimum, implement `id` and either `prompt` or `stream`. + +## Worker configuration + +Register external worker processes via `registerWorker()`: + +```typescript +server.registerWorker({ + id: 'py-agent', + command: 'python', + args: ['agent.py'], + env: { OPENAI_API_KEY: process.env.OPENAI_API_KEY ?? '' }, + capabilities: ['data-analysis'], +}); +``` + +### WorkerConfig + +```typescript +interface WorkerConfig { + id: string; // Unique worker/agent ID + command: string; // Executable to spawn + args: string[]; // Command-line arguments + env?: Record; // Additional environment variables + capabilities?: string[]; // Advertised capabilities +} +``` + +## Backpressure + +`maxConcurrentRequests` (default: 50) limits the number of in-flight tool handler calls. When the limit is reached, new requests are rejected with a retryable `BridgeError.transport('Server overloaded')`. + +## Input size validation + +- `maxPromptBytes` (default: 1,048,576 / 1 MiB) — maximum prompt size in bytes +- `maxMetadataBytes` (default: 65,536 / 64 KiB) — maximum metadata size in bytes (JSON-serialized) + +## Session limits + +The `InProcessExecutor` enforces a configurable `maxSessions` limit (default: 100). Sessions also have: + +- **Session TTL** (`sessionTtlMs`, default: 3,600,000 / 1 hour) — maximum session lifetime +- **Idle timeout** (`sessionIdleMs`, default: 600,000 / 10 minutes) — maximum idle time before expiry + +## RuntimeParams + +### Fields + +| Field | Type | Range | Description | +|-------|------|-------|-------------| +| `model` | `string` | — | Model identifier | +| `temperature` | `number` | 0–2 | Sampling temperature | +| `maxTokens` | `number` | positive int | Max tokens to generate | +| `topP` | `number` | 0–1 | Nucleus sampling | +| `topK` | `number` | positive int | Top-K sampling | +| `stopSequences` | `string[]` | — | Stop sequences | +| `systemPrompt` | `string` | — | System prompt override | +| `providerSpecific` | `Record` | — | Provider-native parameters | + +### Merge priority + +``` +ProviderConfig.defaults < session metadata.runtimeParams < prompt-level runtimeParams +``` + +Only defined (non-`undefined`) fields from higher-priority layers override lower ones. `providerSpecific` is shallow-merged across all layers. + +## Peer dependencies + +Provider SDKs are peer/optional dependencies. Install only the SDKs you need: + +```bash +npm install openai # OpenAI +npm install @anthropic-ai/sdk # Anthropic +npm install @google/generative-ai # Google Gemini +``` + +If a provider SDK is not installed, the factory throws `BridgeError.config` with an installation instruction. + +## CLI entry point + +The CLI (`src/cli/server.ts`) is a reference/diagnostics server with no agents. It starts the MCP server and warns on stderr. Use `bridge_health` and `agents_discover` for diagnostics. For actual delegation, create your own entry point with `server.register()` before `server.start()`. + +## Low-level API (class-based, deprecated) + +The class-based API (`new OpenAIProvider(config)`, `new ProviderRegistry()`, manual wiring) still works but is deprecated. Use the Factory API above instead. + +```typescript +// ⚠️ Deprecated — use openAI(), anthropic(), gemini() factories instead +import { OpenAIProvider, ProviderRegistry, MultiProviderCompanionAgent } from '@stdiobus/mcp-agentic'; + +const registry = new ProviderRegistry(); +registry.register(new OpenAIProvider({ + credentials: { apiKey: process.env.OPENAI_API_KEY ?? '' }, + models: ['gpt-4o'], +})); + +const agent = new MultiProviderCompanionAgent({ + id: 'companion', + defaultProviderId: 'openai', + registry, +}); +``` diff --git a/mcp-agentic/steering/delegation-and-session-lifecycle.md b/mcp-agentic/steering/delegation-and-session-lifecycle.md new file mode 100644 index 0000000..c81bff9 --- /dev/null +++ b/mcp-agentic/steering/delegation-and-session-lifecycle.md @@ -0,0 +1,246 @@ +# Delegation and Session Lifecycle + +## Preferred delegation sequence + +Follow this sequence for reliable delegation: + +1. **Verify bridge readiness** — `bridge_health` if uncertain +2. **Discover agents** — `agents_discover` to find available agents and their providers +3. **Select provider** — if the agent supports multiple providers, choose one based on the `providers` field in the discovery response +4. **Create a session** — `sessions_create` with `agentId` and optional `metadata.provider` (returns `sessionId`) +5. **Submit the prompt** — `sessions_prompt` with `sessionId`, `prompt`, and optional `runtimeParams` +6. **Check status** — `sessions_status` if the task is long-running +7. **Continue session** — `sessions_prompt` again for follow-up requests (with optional `runtimeParams` overrides) +8. **Close session** — `sessions_close` when work is complete + +For one-shot tasks, use `tasks_delegate` (with optional `runtimeParams`) instead of steps 3–8. + +## Session lifecycle states + +Sessions progress through these states: + +- **active** — session created, ready for requests +- **busy** — session is processing a prompt +- **idle** — prompt completed, session ready for more requests +- **failed** — session encountered an unrecoverable error + +> **Note:** When a session is closed (via `sessions_close` or automatic expiry), it is **deleted** from the executor — not transitioned to a "closed" state. You cannot query a closed session via `sessions_status`; it will return "Session not found". + +### State transitions: + +``` +[create] → active → busy → idle → busy → ... + ↓ + failed + [close/expiry] → session deleted (not queryable) +``` + +Sessions also expire automatically (**in-process executor only**): +- **TTL expiry** — session exceeded maximum lifetime (default: 1 hour) +- **Idle expiry** — session idle beyond threshold (default: 10 minutes) + +Expired sessions are reaped and the agent's `onSessionClose` hook is called with reason `'expired'`. + +> **Note:** The `WorkerExecutor` does not have automatic session expiry. Worker sessions persist in the local session map until explicitly closed via `sessions_close`. Worker processes manage their own internal session lifecycle independently. + +## Provider-aware sessions + +When using `MultiProviderCompanionAgent`, sessions can be bound to a specific AI provider at creation time. + +### Creating a session with a specific provider: + +Pass `metadata.provider` to `sessions_create` to select the AI provider for the session: + +``` +sessions_create({ + agentId: "multi-provider-agent", + metadata: { + provider: "anthropic", + runtimeParams: { model: "claude-sonnet-4-20250514", temperature: 0.7 } + } +}) +→ { sessionId: "abc-123", agentId: "multi-provider-agent", status: "active" } +``` + +- `metadata.provider` — selects the AI provider for all prompts in this session +- `metadata.runtimeParams` — sets session-level default parameters (model, temperature, etc.) +- If `metadata.provider` is omitted, the agent uses its configured `defaultProviderId` +- If `metadata.provider` specifies an unregistered provider, `sessions_create` fails with `BridgeError` CONFIG + +### Provider affinity: + +Once a session is created with a specific provider, all prompts in that session use that provider. The provider cannot be changed mid-session — create a new session to switch providers. + +## Runtime parameter overrides + +`RuntimeParams` allow dynamic control of AI generation parameters at each request, without restarting the server. + +### Available parameters: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `model` | `string` | Model identifier (e.g., `"gpt-4o"`, `"claude-sonnet-4-20250514"`) | +| `temperature` | `number` (0–2) | Sampling temperature; higher = more random | +| `maxTokens` | `number` (positive int) | Maximum tokens to generate | +| `topP` | `number` (0–1) | Nucleus sampling probability | +| `topK` | `number` (positive int) | Top-K sampling parameter | +| `stopSequences` | `string[]` | Sequences that stop generation | +| `systemPrompt` | `string` | System prompt override for this request | +| `providerSpecific` | `Record` | Provider-native parameters not covered by common fields | + +### Merge priority (ascending): + +``` +ProviderConfig.defaults < session metadata.runtimeParams < prompt-level runtimeParams +``` + +Only defined fields override lower-priority values. `undefined` fields are ignored during merge. `providerSpecific` is shallow-merged across all layers. + +### Per-prompt overrides via `sessions_prompt`: + +``` +sessions_prompt({ + sessionId: "abc-123", + prompt: "Explain quantum computing", + runtimeParams: { + temperature: 0.2, + maxTokens: 500, + systemPrompt: "You are a physics professor. Explain concepts simply." + } +}) +→ { text: "Quantum computing uses...", stopReason: "end_turn", usage: { inputTokens: 45, outputTokens: 120 } } +``` + +### One-shot delegation with `runtimeParams`: + +``` +tasks_delegate({ + agentId: "multi-provider-agent", + prompt: "Summarize this document", + metadata: { provider: "openai" }, + runtimeParams: { temperature: 0, maxTokens: 200 } +}) +→ { sessionId: "xyz-789", text: "The document covers...", stopReason: "end_turn", usage: { inputTokens: 80, outputTokens: 50 } } +``` + +## Session management rules + +### Always use `sessionId`: + +- `sessions_create` returns a `sessionId` — use it for all subsequent calls +- Pass `sessionId` to `sessions_prompt`, `sessions_status`, `sessions_close`, `sessions_cancel` +- Report `sessionId` in all outputs for traceability + +### One session per task: + +- Don't split related work across sessions +- Only create parallel sessions when the user explicitly requests it +- Reuse sessions to reduce overhead + +### Session invalidation: + +If a session becomes invalid: + +1. **Report explicitly** — tell the user the session is no longer valid +2. **Explain reason** — why the session was invalidated (expired, failed, closed) +3. **Offer recovery** — suggest creating a new session +4. **Preserve context** — maintain conversation context despite session loss + +## Delegation patterns + +### One-shot delegation: + +For single tasks that don't need follow-up: + +``` +tasks_delegate({ + agentId: "my-agent", + prompt: "Analyze this function for bugs" +}) +→ { sessionId: "abc-123", text: "Found 2 issues...", stopReason: "end_turn" } +``` + +`tasks_delegate` creates a session, sends the prompt, and closes the session in one call. + +### Multi-turn delegation: + +For interactive work requiring multiple exchanges: + +``` +sessions_create({ agentId: "my-agent" }) +→ { sessionId: "abc-123", agentId: "my-agent", status: "active" } + +sessions_prompt({ sessionId: "abc-123", prompt: "Analyze this codebase" }) +→ { text: "Found 3 issues...", stopReason: "end_turn" } + +sessions_prompt({ sessionId: "abc-123", prompt: "Now suggest fixes" }) +→ { text: "Here are the fixes...", stopReason: "end_turn" } + +sessions_close({ sessionId: "abc-123" }) +→ { closed: true, sessionId: "abc-123" } +``` + +### Cancellation: + +To cancel an in-flight prompt: + +``` +sessions_cancel({ sessionId: "abc-123", requestId: "req-456" }) +``` + +The agent's `cancel` hook is called if implemented. + +## Structured payloads + +### Preserve structured outputs: + +- **Don't transform** — pass results through unchanged +- **Don't summarize** — keep full structured data +- **Don't flatten** — maintain nested structure +- **Don't filter** — include all fields unless explicitly requested + +### Prompt result format: + +```json +{ + "text": "Agent response text", + "stopReason": "end_turn", + "requestId": "req-456", + "usage": { + "inputTokens": 100, + "outputTokens": 250 + } +} +``` + +The `usage` field is present when the AI provider reports token consumption. It contains `inputTokens` (tokens consumed by the prompt) and `outputTokens` (tokens generated in the response). Not all providers or configurations guarantee usage data. + +### Session info format: + +```json +{ + "sessionId": "abc-123", + "agentId": "my-agent", + "status": "idle", + "createdAt": 1719000000000, + "lastActivityAt": 1719000060000 +} +``` + +## Session cleanup + +### When to close sessions: + +- Task is complete and no follow-up is expected +- User explicitly requests to stop +- Session has been idle too long (automatic via idle expiry) +- Unrecoverable error occurred + +### Automatic cleanup (in-process executor only): + +- **TTL expiry** — sessions expire after configured TTL (default: 1 hour) +- **Idle timeout** — sessions close after idle threshold (default: 10 minutes) +- **Max sessions** — new sessions are rejected when capacity is reached +- The `InProcessExecutor` runs a periodic reaper that cleans up expired sessions + +> Worker sessions are not automatically reaped. They persist until explicitly closed or the server shuts down. diff --git a/mcp-agentic/steering/discovery-and-routing.md b/mcp-agentic/steering/discovery-and-routing.md new file mode 100644 index 0000000..d47f958 --- /dev/null +++ b/mcp-agentic/steering/discovery-and-routing.md @@ -0,0 +1,189 @@ +# Discovery and Routing + +## Discovery workflow + +Discovery **must happen before delegation** when the target agent is not explicitly known. + +### Discovery sequence: + +1. **Check bridge health** — verify the bridge is operational via `bridge_health` +2. **Call `agents_discover`** — retrieve list of available agents +3. **Filter by capability** — pass `capability` parameter to narrow results +4. **Select agent** — choose the most appropriate agent by ID +5. **Create session** — use the selected `agentId` in `sessions_create` or `tasks_delegate` + +### Discovery timing: + +- **On first use** — always discover on initial power activation +- **On explicit request** — when user asks to see available agents +- **On routing failure** — when selected agent is unavailable +- **Never speculatively** — don't discover without a concrete need + +## Agent resolution + +`McpAgenticServer` resolves agents across two executor backends: + +1. **InProcessExecutor** — agents registered via `register()` +2. **WorkerExecutor** — agents registered via `registerWorker()` + +### Resolution priority: + +In-process agents **always take priority** over workers. When an `agentId` exists in both executors, the in-process agent handles the request. + +### Resolution flow: + +1. Check executor cache (populated on `start()`) +2. On cache miss, call `discover()` on `InProcessExecutor` first +3. If not found, call `discover()` on `WorkerExecutor` +4. Cache the result for subsequent lookups +5. Cache is invalidated on `register()` or `registerWorker()` + +### Default agent: + +- If `defaultAgentId` is set in `McpAgenticServerConfig`, it is used when no `agentId` is specified +- If no `defaultAgentId` and no `agentId`, the `InProcessExecutor` uses the first registered agent +- If no in-process agents exist, the `WorkerExecutor` is used + +## Agent info + +Agents expose their identity and capabilities through discovery: + +```json +{ + "id": "my-agent", + "capabilities": ["code-analysis", "debugging"], + "status": "ready" +} +``` + +When an agent supports multiple AI providers (e.g., `MultiProviderCompanionAgent`), the discovery response includes an optional `providers` field with enriched metadata: + +```json +{ + "id": "multi-provider-agent", + "capabilities": ["general"], + "status": "ready", + "providers": [ + { + "id": "openai", + "models": ["gpt-4o", "gpt-4o-mini"], + "kind": "llm", + "capabilities": { "streaming": true, "tools": true, "vision": true, "jsonMode": true }, + "displayName": "OpenAI" + }, + { + "id": "anthropic", + "models": ["claude-sonnet-4-20250514"], + "kind": "llm", + "capabilities": { "streaming": true, "tools": true, "vision": true, "jsonMode": false }, + "displayName": "Anthropic" + }, + { + "id": "google-gemini", + "models": ["gemini-2.0-flash"], + "kind": "llm", + "capabilities": { "streaming": false, "tools": false, "vision": true, "jsonMode": true }, + "displayName": "Google Gemini" + } + ] +} +``` + +Each provider entry may include: +- `kind` — provider type (`'llm'`, `'embedding'`, `'reranker'`). Defaults to `'llm'` if not set. +- `capabilities` — self-reported capabilities (`streaming`, `tools`, `vision`, `jsonMode`). Omitted if the provider has no capabilities metadata. +- `displayName` — human-readable name (e.g., `"OpenAI"`, `"Google Gemini"`) +- `description` — provider description + +The `providers` field is only present when the agent has a `ProviderRegistry` with registered providers. Agents without multi-provider support omit this field entirely. + +### Agent statuses: + +- **ready** — agent is available for new sessions + +> **Note:** The `AgentInfo` type also defines `'busy'` and `'unavailable'` statuses, but the current executor implementations always return `'ready'`. Dynamic status tracking (e.g., marking agents as busy when all sessions are in use) may be added in a future version. + +### Capability filtering: + +Pass `capability` to `agents_discover` to filter results: + +``` +agents_discover({ capability: "code-analysis" }) +→ [{ id: "my-agent", capabilities: ["code-analysis", "debugging"], status: "ready" }] +``` + +Only agents whose `capabilities` array includes the specified capability are returned. + +## Provider discovery + +When agents support multiple AI providers, use `agents_discover` to find available providers and their models before creating a session. + +### Discovery sequence: + +1. **Call `agents_discover`** — retrieve agent list including provider information +2. **Inspect `providers` field** — check which AI providers are available and their supported models +3. **Select provider** — choose the appropriate provider based on task requirements +4. **Create session with provider** — pass `metadata.provider` to `sessions_create` + +### Provider selection rules: + +- **By model availability** — choose the provider that supports the model you need (e.g., `gpt-4o` → `openai`, `claude-sonnet-4-20250514` → `anthropic`) +- **By capability match** — some providers may be better suited for specific tasks (e.g., code generation, creative writing) +- **By cost/latency tradeoff** — different providers have different pricing and response times +- **Default provider** — when no provider is specified, the agent uses its configured `defaultProviderId` + +### Example: discover providers and select one + +``` +agents_discover({}) +→ [{ + id: "multi-provider-agent", + capabilities: ["general"], + status: "ready", + providers: [ + { id: "openai", models: ["gpt-4o", "gpt-4o-mini"] }, + { id: "anthropic", models: ["claude-sonnet-4-20250514"] } + ] + }] + +sessions_create({ + agentId: "multi-provider-agent", + metadata: { provider: "anthropic" } +}) +→ { sessionId: "abc-123", agentId: "multi-provider-agent", status: "active" } +``` + +### Provider not found: + +If `metadata.provider` specifies an unregistered provider id, `sessions_create` fails with `BridgeError.config('Provider "{id}" is not registered in the ProviderRegistry')`. + +## Routing constraints + +- **Use discovery results as source of truth** — never invent agents +- **Preserve session affinity** — all requests in a session go to the same executor +- **Never silently reroute** — report any routing changes explicitly +- **Validate before routing** — ensure agent is available and capable + +## Session-to-executor binding + +Once a session is created: + +- The session is bound to the executor that created it +- `McpAgenticServer` resolves the executor for session-based operations (`sessions_prompt`, `sessions_status`, `sessions_close`, `sessions_cancel`) by checking which executor owns the `sessionId` +- In-process executor is checked first, then worker executor + +## Routing failures + +When routing fails: + +1. **Report the error** — include `BridgeError` type and message +2. **Explain reason** — agent not found, no agents registered, etc. +3. **Suggest alternatives** — use `agents_discover` to find available agents +4. **Preserve session** — keep session valid if possible + +### Common routing failures: + +- **Agent not found** — `BridgeError.upstream('Agent not found: {agentId}')` +- **No agents registered** — `BridgeError.config('No agents registered')` +- **Session not found** — `BridgeError.upstream('Session not found: {sessionId}')` +- **Executor not started** — `BridgeError.internal('Executor not started')` diff --git a/mcp-agentic/steering/failure-handling.md b/mcp-agentic/steering/failure-handling.md new file mode 100644 index 0000000..4f814ab --- /dev/null +++ b/mcp-agentic/steering/failure-handling.md @@ -0,0 +1,238 @@ +# Failure Handling + +## BridgeError categories + +All errors in the system are represented as `BridgeError` instances with a `type` field indicating the category: + +| Category | When Used | Retryable | MCP Error Code | +|----------|-----------|-----------|----------------| +| `CONFIG` | Invalid configuration, no agents registered, missing provider credentials, unregistered provider id | No | `-32004` | +| `AUTH` | Invalid API key or authentication failure from AI provider SDKs | No | `-32003` | +| `UPSTREAM` | Agent errors, session not found, agent not found, invalid worker response, provider SDK errors (bad request, rate limit, server errors) | Varies | `-32002` | +| `TRANSPORT` | StdioBus communication failures, worker timeouts, server overloaded (backpressure), provider network/connection errors | Yes | `-32000` | +| `TIMEOUT` | Provider SDK request timeouts | Yes | `-32001` | +| `INTERNAL` | Executor not started, unexpected internal errors | No | `-32603` | + +> **Note:** The `AUTH` and `TIMEOUT` categories are now actively used by AI provider implementations (OpenAI, Anthropic, Google Gemini) for authentication failures and request timeouts respectively. The `PROTOCOL` type remains reserved for future use. + +## Error sources by executor + +### InProcessExecutor errors: + +| Scenario | Error | +|----------|-------| +| `prompt()` called before `start()` | `BridgeError.internal('Executor not started')` | +| `createSession()` with non-existent `agentId` | `BridgeError.upstream('Agent not found: {agentId}')` | +| `createSession()` at capacity | `BridgeError.upstream('Session capacity reached')` | +| `getSession()` / `prompt()` with unknown `sessionId` | `BridgeError.upstream('Session not found: {sessionId}')` | +| Agent's `prompt()` throws unexpected error | `BridgeError.upstream('Agent {agentId} failed', {}, cause)` | +| No agents registered | `BridgeError.config('No agents registered')` | + +### WorkerExecutor errors: + +| Scenario | Error | +|----------|-------| +| `start()` fails to create StdioBus | `BridgeError.transport('Failed to start StdioBus')` | +| `bus.request()` throws error | `BridgeError.upstream('Worker {workerId} failed: {operation}', {}, cause)` | +| `bus.request()` times out | `BridgeError.transport('Worker {workerId} timed out', { retryable: true })` | +| `prompt()` called before `start()` | `BridgeError.internal('Executor not started')` | +| Invalid worker response (missing `sessionId`) | `BridgeError.upstream('Invalid worker response: missing sessionId')` | +| Malformed prompt result | `BridgeError.upstream('Invalid worker response: malformed prompt result')` | + +### McpAgenticServer errors: + +| Scenario | Error | +|----------|-------| +| Concurrent request limit reached | `BridgeError.transport('Server overloaded', { retryable: true })` | +| Prompt exceeds `maxPromptBytes` | `BridgeError.upstream('Prompt exceeds maximum size')` | +| Metadata exceeds `maxMetadataBytes` | `BridgeError.upstream('Metadata exceeds maximum size')` | + +### AI provider errors: + +Each provider maps native SDK errors to `BridgeError` with the appropriate category. All provider errors include `providerId` in the error details. + +#### OpenAI Provider (`openai` SDK): + +| SDK Error | BridgeError Category | Retryable | +|-----------|---------------------|-----------| +| `AuthenticationError` (401) | AUTH | No | +| `RateLimitError` (429) | UPSTREAM | Yes | +| `APIConnectionError` | TRANSPORT | Yes | +| `APITimeoutError` | TIMEOUT | Yes | +| `BadRequestError` (400) | UPSTREAM | No | +| `InternalServerError` (500+) | UPSTREAM | Yes | +| Unknown error | UPSTREAM | No | + +#### Anthropic Provider (`@anthropic-ai/sdk`): + +| SDK Error | BridgeError Category | Retryable | +|-----------|---------------------|-----------| +| `AuthenticationError` (401) | AUTH | No | +| `RateLimitError` (429) | UPSTREAM | Yes | +| `APIConnectionError` | TRANSPORT | Yes | +| `APIConnectionTimeoutError` | TIMEOUT | Yes | +| `BadRequestError` (400) | UPSTREAM | No | +| `InternalServerError` (500+) | UPSTREAM | Yes | +| Unknown error | UPSTREAM | No | + +#### Google Gemini Provider (`@google/generative-ai`): + +| SDK Error | BridgeError Category | Retryable | +|-----------|---------------------|-----------| +| Error with status `UNAUTHENTICATED` | AUTH | No | +| Error with status `RESOURCE_EXHAUSTED` | UPSTREAM | Yes | +| Network/fetch errors | TRANSPORT | Yes | +| Timeout errors | TIMEOUT | Yes | +| Error with status `INVALID_ARGUMENT` | UPSTREAM | No | +| Error with status `INTERNAL` | UPSTREAM | Yes | +| Unknown error | UPSTREAM | No | + +### Provider error examples: + +``` +# Invalid API key +BridgeError AUTH: "Incorrect API key provided" { providerId: "openai", retryable: false } + +# Rate limiting +BridgeError UPSTREAM: "Rate limit exceeded" { providerId: "anthropic", retryable: true } + +# Network error +BridgeError TRANSPORT: "Connection error" { providerId: "google-gemini", retryable: true } + +# Request timeout +BridgeError TIMEOUT: "Request timed out" { providerId: "openai", retryable: true } + +# Missing credentials at construction +BridgeError CONFIG: "Missing required credential: apiKey" { retryable: false } + +# Unregistered provider +BridgeError CONFIG: 'Provider "unknown" is not registered in the ProviderRegistry' { retryable: false } +``` + +## Failure reporting principles + +When a tool call fails, **always report**: + +1. **Error category** — the `BridgeError` type (CONFIG, UPSTREAM, TRANSPORT, INTERNAL) +2. **Error message** — human-readable description +3. **Retryability** — whether the operation can be retried (`details.retryable`) +4. **Session validity** — whether the session remains valid (`details.sessionValid`) +5. **Recovery options** — what the user can do next + +> **Note:** Tool handlers may also produce non-BridgeError exceptions (e.g., Zod validation errors for malformed input). These are caught by `mapErrorToMCP()` and mapped to `INTERNAL` with `retryable: false`. + +## Error classification + +### Transient errors (retryable): + +- Server overloaded (backpressure) — `TRANSPORT` +- Worker timeout — `TRANSPORT` with `retryable: true` +- StdioBus transport failures — `TRANSPORT` +- Provider rate limiting — `UPSTREAM` with `retryable: true` +- Provider network/connection errors — `TRANSPORT` with `retryable: true` +- Provider request timeouts — `TIMEOUT` with `retryable: true` +- Provider internal server errors (500+) — `UPSTREAM` with `retryable: true` + +**Strategy:** Wait and retry. + +### Authentication errors (not retryable): + +- Invalid API key — `AUTH` + +**Strategy:** Report to user, fix the API key, don't retry. + +### Permanent errors (not retryable): + +- Invalid configuration +- Agent not found +- Session not found +- Session capacity reached +- Input size exceeded +- Missing provider credentials — `CONFIG` +- Unregistered provider id — `CONFIG` +- Bad request to provider (invalid parameters) — `UPSTREAM` + +**Strategy:** Report to user, fix the issue, don't retry. + +### Internal errors (not retryable): + +- Executor not started +- Unexpected internal failures + +**Strategy:** Report to user, check bridge health, restart if needed. + +## MCP error mapping + +`BridgeError` instances are mapped to MCP JSON-RPC error codes via `mapErrorToMCP()`: + +``` +CONFIG → -32004 (Config Error) +AUTH → -32003 (Auth Error) +UPSTREAM → -32002 (Upstream Error) +TIMEOUT → -32001 (Timeout Error) +TRANSPORT → -32000 (Server Error) +INTERNAL → -32603 (Internal Error) +Unknown → -32603 (Internal Error) +``` + +> `PROTOCOL` → `-32000` is defined in the error mapper but not currently produced by any runtime code path. + +The MCP error response includes: +- `code` — numeric MCP error code +- `message` — original error message +- `data` — error details including `type`, `retryable`, `sessionValid` + +## Failure handling rules + +### Do NOT: + +- **Claim success on failure** — be explicit about errors +- **Hide bridge errors** — don't wrap in generic messages +- **Silently discard warnings** — report all warnings +- **Retry indefinitely** — respect the `retryable` flag +- **Ignore session state** — check if session is still valid after errors + +### DO: + +- **Report exact error category** — use the `BridgeError` type +- **Distinguish retryable from permanent** — check `details.retryable` +- **Preserve `sessionId`** — keep session identifier even on failure +- **Include raw error details** — provide underlying error context +- **Suggest recovery** — offer actionable next steps +- **Use `bridge_health`** — check readiness after failures + +## Idempotent operations (safe to retry): + +- `bridge_health` +- `agents_discover` +- `sessions_status` +- `sessions_close` (returns silently if session already closed or doesn't exist) + +## Non-idempotent operations (retry with caution): + +- `sessions_create` +- `sessions_prompt` +- `sessions_cancel` +- `tasks_delegate` + +Only retry these if the error is marked `retryable: true`. + +## User communication + +### Error messages for users: + +- **Be specific** — explain exactly what failed and which error category +- **Be actionable** — suggest what the user can do +- **Be honest** — don't hide or minimize failures +- **Be concise** — include the error type and message, not full stack traces + +### Example user message: + +``` +Failed to submit prompt to agent 'my-agent': +Session not found: abc-123 (UPSTREAM, not retryable) + +The session may have expired. You can: +1. Create a new session with sessions_create +2. Check available agents with agents_discover +```