diff --git a/.github/agents/docs-maintenance.agent.md b/.github/agents/docs-maintenance.agent.md new file mode 100644 index 00000000..9b605c26 --- /dev/null +++ b/.github/agents/docs-maintenance.agent.md @@ -0,0 +1,462 @@ +--- +description: Audit SDK documentation and generate an actionable improvement plan. +tools: + - grep + - glob + - view + - create + - edit +--- + +# SDK Documentation Maintenance Agent + +You are a documentation auditor for the GitHub Copilot SDK. Your job is to analyze the documentation and **produce a prioritized action plan** of improvements needed. + +## IMPORTANT: Output Format + +**You do NOT make changes directly.** Instead, you: + +1. **Audit** the documentation against the standards below +2. **Generate a plan** as a markdown file with actionable items + +The human will then review the plan and selectively ask Copilot to implement specific items. + +> **Note:** When run from github.com, the platform will automatically create a PR with your changes. When run locally, you just create the file. + +### Plan Output Format + +Create a file called `docs/IMPROVEMENT_PLAN.md` with this structure: + +```markdown +# Documentation Improvement Plan + +Generated: [date] +Audited by: docs-maintenance agent + +## Summary + +- **Coverage**: X% of SDK features documented +- **Sample Accuracy**: X issues found +- **Link Health**: X broken links +- **Multi-language**: X missing examples + +## Critical Issues (Fix Immediately) + +### 1. [Issue Title] +- **File**: `docs/path/to/file.md` +- **Line**: ~42 +- **Problem**: [description] +- **Fix**: [specific action to take] + +### 2. ... + +## High Priority (Should Fix Soon) + +### 1. [Issue Title] +- **File**: `docs/path/to/file.md` +- **Problem**: [description] +- **Fix**: [specific action to take] + +## Medium Priority (Nice to Have) + +### 1. ... + +## Low Priority (Future Improvement) + +### 1. ... + +## Missing Documentation + +The following SDK features lack documentation: + +- [ ] `feature_name` - needs new doc at `docs/path/suggested.md` +- [ ] ... + +## Sample Code Fixes Needed + +The following code samples don't match the SDK interface: + +### File: `docs/example.md` + +**Line ~25 - TypeScript sample uses wrong method name:** +```typescript +// Current (wrong): +await client.create_session() + +// Should be: +await client.createSession() +``` + +**Line ~45 - Python sample has camelCase:** +```python +# Current (wrong): +client = CopilotClient(cliPath="/usr/bin/copilot") + +# Should be: +client = CopilotClient(cli_path="/usr/bin/copilot") +``` + +## Broken Links + +| Source File | Line | Broken Link | Suggested Fix | +|-------------|------|-------------|---------------| +| `docs/a.md` | 15 | `./missing.md` | Remove or create file | + +## Consistency Issues + +- [ ] Term "XXX" used inconsistently (file1.md says "A", file2.md says "B") +- [ ] ... +``` + +After creating this plan file, your work is complete. The platform (github.com) will handle creating a PR if applicable. + +## Documentation Standards + +The SDK documentation must meet these quality standards: + +### 1. Feature Coverage + +Every major SDK feature should be documented. Core features include: + +**Client & Connection:** +- Client initialization and configuration +- Connection modes (stdio vs TCP) +- Authentication options +- Auto-start and auto-restart behavior + +**Session Management:** +- Creating sessions +- Resuming sessions +- Destroying/deleting sessions +- Listing sessions +- Infinite sessions and compaction + +**Messaging:** +- Sending messages +- Attachments (file, directory, selection) +- Streaming responses +- Aborting requests + +**Tools:** +- Registering custom tools +- Tool schemas (JSON Schema) +- Tool handlers +- Permission handling + +**Hooks:** +- Pre-tool use (permission control) +- Post-tool use (result modification) +- User prompt submitted +- Session start/end +- Error handling + +**MCP Servers:** +- Local/stdio servers +- Remote HTTP/SSE servers +- Configuration options +- Debugging MCP issues + +**Events:** +- Event subscription +- Event types +- Streaming vs final events + +**Advanced:** +- Custom providers (BYOK) +- System message customization +- Custom agents +- Skills + +### 2. Multi-Language Support + +All documentation must include examples for all four SDKs: +- **Node.js / TypeScript** +- **Python** +- **Go** +- **.NET (C#)** + +Use collapsible `
` sections with the first language open by default. + +### 3. Content Structure + +Each documentation file should include: +- Clear title and introduction +- Table of contents for longer docs +- Code examples for all languages +- Reference tables for options/parameters +- Common patterns and use cases +- Best practices section +- "See Also" links to related docs + +### 4. Link Integrity + +All internal links must: +- Point to existing files +- Use relative paths (e.g., `./hooks/overview.md`, `../debugging.md`) +- Include anchor links where appropriate (e.g., `#session-start`) + +### 5. Consistency + +Maintain consistency in: +- Terminology (use same terms across all docs) +- Code style (consistent formatting in examples) +- Section ordering (similar docs should have similar structure) +- Voice and tone (clear, direct, developer-friendly) + +## Audit Checklist + +When auditing documentation, check: + +### Completeness +- [ ] All major SDK features are documented +- [ ] All four languages have examples +- [ ] API reference covers all public methods +- [ ] Configuration options are documented +- [ ] Error scenarios are explained + +### Accuracy +- [ ] Code examples are correct and runnable +- [ ] Type signatures match actual SDK types +- [ ] Default values are accurate +- [ ] Behavior descriptions match implementation + +### Links +- [ ] All internal links resolve to existing files +- [ ] External links are valid and relevant +- [ ] Anchor links point to existing sections + +### Discoverability +- [ ] Clear navigation between related topics +- [ ] Consistent "See Also" sections +- [ ] Searchable content (good headings, keywords) +- [ ] README links to key documentation + +### Clarity +- [ ] Jargon is explained or avoided +- [ ] Examples are practical and realistic +- [ ] Complex topics have step-by-step explanations +- [ ] Error messages are helpful + +## Documentation Structure + +The expected documentation structure is: + +``` +docs/ +├── getting-started.md # Quick start tutorial +├── debugging.md # General debugging guide +├── compatibility.md # SDK vs CLI feature comparison +├── hooks/ +│ ├── overview.md # Hooks introduction +│ ├── pre-tool-use.md # Permission control +│ ├── post-tool-use.md # Result transformation +│ ├── user-prompt-submitted.md +│ ├── session-lifecycle.md +│ └── error-handling.md +└── mcp/ + ├── overview.md # MCP configuration + └── debugging.md # MCP troubleshooting +``` + +Additional directories to consider: +- `docs/tools/` - Custom tool development +- `docs/events/` - Event reference +- `docs/advanced/` - Advanced topics (providers, agents, skills) +- `docs/api/` - API reference (auto-generated or manual) + +## Audit Process + +### Step 1: Inventory Current Docs + +```bash +# List all documentation files +find docs -name "*.md" -type f | sort + +# Check for README references +grep -r "docs/" README.md +``` + +### Step 2: Check Feature Coverage + +Compare documented features against SDK types: + +```bash +# Node.js types +grep -E "export (interface|type|class)" nodejs/src/types.ts nodejs/src/client.ts nodejs/src/session.ts + +# Python types +grep -E "^class |^def " python/copilot/types.py python/copilot/client.py python/copilot/session.py + +# Go types +grep -E "^type |^func " go/types.go go/client.go go/session.go + +# .NET types +grep -E "public (class|interface|enum)" dotnet/src/Types.cs dotnet/src/Client.cs dotnet/src/Session.cs +``` + +### Step 3: Validate Links + +```bash +# Find all markdown links +grep -roh '\[.*\](\..*\.md[^)]*' docs/ + +# Check each link exists +for link in $(grep -roh '\](\..*\.md' docs/ | sed 's/\](//' | sort -u); do + # Resolve relative to docs/ + if [ ! -f "docs/$link" ]; then + echo "Broken link: $link" + fi +done +``` + +### Step 4: Check Multi-Language Examples + +```bash +# Ensure all docs have examples for each language +for file in $(find docs -name "*.md"); do + echo "=== $file ===" + grep -c "Node.js\|TypeScript" "$file" || echo "Missing Node.js" + grep -c "Python" "$file" || echo "Missing Python" + grep -c "Go" "$file" || echo "Missing Go" + grep -c "\.NET\|C#" "$file" || echo "Missing .NET" +done +``` + +### Step 5: Validate Code Samples Against SDK Interface + +**CRITICAL**: All code examples must match the actual SDK interface. Verify method names, parameter names, types, and return values. + +#### Node.js/TypeScript Validation + +Check that examples use correct method signatures: + +```bash +# Extract public methods from SDK +grep -E "^\s*(async\s+)?[a-z][a-zA-Z]+\(" nodejs/src/client.ts nodejs/src/session.ts | head -50 + +# Key interfaces to verify against +cat nodejs/src/types.ts | grep -A 20 "export interface CopilotClientOptions" +cat nodejs/src/types.ts | grep -A 50 "export interface SessionConfig" +cat nodejs/src/types.ts | grep -A 20 "export interface SessionHooks" +cat nodejs/src/types.ts | grep -A 10 "export interface ExportSessionOptions" +``` + +**Must match:** +- `CopilotClient` constructor options: `cliPath`, `cliUrl`, `useStdio`, `port`, `logLevel`, `autoStart`, `autoRestart`, `env`, `githubToken`, `useLoggedInUser` +- `createSession()` config: `model`, `tools`, `hooks`, `systemMessage`, `mcpServers`, `availableTools`, `excludedTools`, `streaming`, `reasoningEffort`, `provider`, `infiniteSessions`, `customAgents`, `workingDirectory` +- `CopilotSession` methods: `send()`, `sendAndWait()`, `getMessages()`, `destroy()`, `abort()`, `on()`, `once()`, `off()` +- Hook names: `onPreToolUse`, `onPostToolUse`, `onUserPromptSubmitted`, `onSessionStart`, `onSessionEnd`, `onErrorOccurred` + +#### Python Validation + +```bash +# Extract public methods +grep -E "^\s+async def [a-z]" python/copilot/client.py python/copilot/session.py + +# Key types +cat python/copilot/types.py | grep -A 20 "class CopilotClientOptions" +cat python/copilot/types.py | grep -A 30 "class SessionConfig" +cat python/copilot/types.py | grep -A 15 "class SessionHooks" +``` + +**Must match (snake_case):** +- `CopilotClient` options: `cli_path`, `cli_url`, `use_stdio`, `port`, `log_level`, `auto_start`, `auto_restart`, `env`, `github_token`, `use_logged_in_user` +- `create_session()` config keys: `model`, `tools`, `hooks`, `system_message`, `mcp_servers`, `available_tools`, `excluded_tools`, `streaming`, `reasoning_effort`, `provider`, `infinite_sessions`, `custom_agents`, `working_directory` +- `CopilotSession` methods: `send()`, `send_and_wait()`, `get_messages()`, `destroy()`, `abort()`, `export_session()` +- Hook names: `on_pre_tool_use`, `on_post_tool_use`, `on_user_prompt_submitted`, `on_session_start`, `on_session_end`, `on_error_occurred` + +#### Go Validation + +```bash +# Extract public methods (capitalized = exported) +grep -E "^func \([a-z]+ \*[A-Z]" go/client.go go/session.go + +# Key types +cat go/types.go | grep -A 20 "type ClientOptions struct" +cat go/types.go | grep -A 30 "type SessionConfig struct" +cat go/types.go | grep -A 15 "type SessionHooks struct" +``` + +**Must match (PascalCase for exported):** +- `ClientOptions` fields: `CLIPath`, `CLIUrl`, `UseStdio`, `Port`, `LogLevel`, `AutoStart`, `AutoRestart`, `Env`, `GithubToken`, `UseLoggedInUser` +- `SessionConfig` fields: `Model`, `Tools`, `Hooks`, `SystemMessage`, `MCPServers`, `AvailableTools`, `ExcludedTools`, `Streaming`, `ReasoningEffort`, `Provider`, `InfiniteSessions`, `CustomAgents`, `WorkingDirectory` +- `Session` methods: `Send()`, `SendAndWait()`, `GetMessages()`, `Destroy()`, `Abort()`, `ExportSession()` +- Hook fields: `OnPreToolUse`, `OnPostToolUse`, `OnUserPromptSubmitted`, `OnSessionStart`, `OnSessionEnd`, `OnErrorOccurred` + +#### .NET Validation + +```bash +# Extract public methods +grep -E "public (async Task|void|[A-Z])" dotnet/src/Client.cs dotnet/src/Session.cs | head -50 + +# Key types +cat dotnet/src/Types.cs | grep -A 20 "public class CopilotClientOptions" +cat dotnet/src/Types.cs | grep -A 40 "public class SessionConfig" +cat dotnet/src/Types.cs | grep -A 15 "public class SessionHooks" +``` + +**Must match (PascalCase):** +- `CopilotClientOptions` properties: `CliPath`, `CliUrl`, `UseStdio`, `Port`, `LogLevel`, `AutoStart`, `AutoRestart`, `Environment`, `GithubToken`, `UseLoggedInUser` +- `SessionConfig` properties: `Model`, `Tools`, `Hooks`, `SystemMessage`, `McpServers`, `AvailableTools`, `ExcludedTools`, `Streaming`, `ReasoningEffort`, `Provider`, `InfiniteSessions`, `CustomAgents`, `WorkingDirectory` +- `CopilotSession` methods: `SendAsync()`, `SendAndWaitAsync()`, `GetMessagesAsync()`, `DisposeAsync()`, `AbortAsync()`, `ExportSessionAsync()` +- Hook properties: `OnPreToolUse`, `OnPostToolUse`, `OnUserPromptSubmitted`, `OnSessionStart`, `OnSessionEnd`, `OnErrorOccurred` + +#### Common Sample Errors to Check + +1. **Wrong method names:** + - ❌ `client.create_session()` in TypeScript (should be `createSession()`) + - ❌ `session.SendAndWait()` in Python (should be `send_and_wait()`) + - ❌ `client.CreateSession()` in Go without context (should be `CreateSession(ctx, config)`) + +2. **Wrong parameter names:** + - ❌ `{ cli_path: "..." }` in TypeScript (should be `cliPath`) + - ❌ `{ cliPath: "..." }` in Python (should be `cli_path`) + - ❌ `McpServers` in Go (should be `MCPServers`) + +3. **Missing required parameters:** + - Go methods require `context.Context` as first parameter + - .NET async methods should use `CancellationToken` + +4. **Wrong hook structure:** + - ❌ `hooks: { preToolUse: ... }` (should be `onPreToolUse`) + - ❌ `hooks: { OnPreToolUse: ... }` in Python (should be `on_pre_tool_use`) + +5. **Outdated APIs:** + - Check for deprecated method names + - Verify against latest SDK version + +#### Validation Script + +Run this to extract all code blocks and check for common issues: + +```bash +# Extract TypeScript examples and check for Python-style naming +grep -A 20 '```typescript' docs/**/*.md | grep -E "cli_path|create_session|send_and_wait" && echo "ERROR: Python naming in TypeScript" + +# Extract Python examples and check for camelCase +grep -A 20 '```python' docs/**/*.md | grep -E "cliPath|createSession|sendAndWait" && echo "ERROR: camelCase in Python" + +# Check Go examples have context parameter +grep -A 20 '```go' docs/**/*.md | grep -E "CreateSession\([^c]|Send\([^c]" && echo "WARNING: Go method may be missing context" +``` + +### Step 6: Create the Plan + +After completing the audit: + +1. Create `docs/IMPROVEMENT_PLAN.md` with all findings organized by priority +2. Your work is complete - the platform handles PR creation + +The human reviewer can then: +- Review the plan +- Comment on specific items to prioritize +- Ask Copilot to implement specific fixes from the plan + +## Remember + +- **You are an auditor, not a fixer** - your job is to find issues and document them clearly +- Each item in the plan should be **actionable** - specific enough that someone (or Copilot) can fix it +- Include **file paths and line numbers** where possible +- Show **before/after code** for sample fixes +- Prioritize issues by **impact on developers** +- The plan becomes the work queue for future improvements diff --git a/README.md b/README.md index 6ee8a4d1..bf74289b 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,19 @@ Billing for the GitHub Copilot SDK is based on the same model as the Copilot CLI ### Does it support BYOK (Bring Your Own Key)? -Yes, the GitHub Copilot SDK supports BYOK (Bring Your Own Key). You can configure the SDK to use your own API keys from supported LLM providers (e.g. OpenAI, Azure, Anthropic) to access models through those providers. Refer to the individual SDK documentation for instructions on setting up BYOK. +Yes, the GitHub Copilot SDK supports BYOK (Bring Your Own Key). You can configure the SDK to use your own API keys from supported LLM providers (e.g. OpenAI, Azure AI Foundry, Anthropic) to access models through those providers. See the **[BYOK documentation](./docs/auth/byok.md)** for setup instructions and examples. + +**Note:** BYOK uses key-based authentication only. Microsoft Entra ID (Azure AD), managed identities, and third-party identity providers are not supported. + +### What authentication methods are supported? + +The SDK supports multiple authentication methods: +- **GitHub signed-in user** - Uses stored OAuth credentials from `copilot` CLI login +- **OAuth GitHub App** - Pass user tokens from your GitHub OAuth app +- **Environment variables** - `COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, `GITHUB_TOKEN` +- **BYOK** - Use your own API keys (no GitHub auth required) + +See the **[Authentication documentation](./docs/auth/index.md)** for details on each method. ### Do I need to install the Copilot CLI separately? @@ -96,6 +108,7 @@ Please use the [GitHub Issues](https://github.com/github/copilot-sdk/issues) pag ## Quick Links - **[Getting Started](./docs/getting-started.md)** – Tutorial to get up and running +- **[Authentication](./docs/auth/index.md)** – GitHub OAuth, BYOK, and more - **[Cookbook](https://github.com/github/awesome-copilot/blob/main/cookbook/copilot-sdk)** – Practical recipes for common tasks across all languages - **[More Resources](https://github.com/github/awesome-copilot/blob/main/collections/copilot-sdk.md)** – Additional examples, tutorials, and community resources diff --git a/docs/auth/byok.md b/docs/auth/byok.md new file mode 100644 index 00000000..05256e97 --- /dev/null +++ b/docs/auth/byok.md @@ -0,0 +1,373 @@ +# BYOK (Bring Your Own Key) + +BYOK allows you to use the Copilot SDK with your own API keys from model providers, bypassing GitHub Copilot authentication. This is useful for enterprise deployments, custom model hosting, or when you want direct billing with your model provider. + +## Supported Providers + +| Provider | Type Value | Notes | +|----------|------------|-------| +| OpenAI | `"openai"` | OpenAI API and OpenAI-compatible endpoints | +| Azure OpenAI / Azure AI Foundry | `"azure"` | Azure-hosted models | +| Anthropic | `"anthropic"` | Claude models | +| Ollama | `"openai"` | Local models via OpenAI-compatible API | +| Other OpenAI-compatible | `"openai"` | vLLM, LiteLLM, etc. | + +## Quick Start: Azure AI Foundry + +Azure AI Foundry (formerly Azure OpenAI) is a common BYOK deployment target for enterprises. Here's a complete example: + +
+Python + +```python +import asyncio +import os +from copilot import CopilotClient + +FOUNDRY_MODEL_URL = "https://your-resource.openai.azure.com/openai/v1/" +# Set FOUNDRY_API_KEY environment variable + +async def main(): + client = CopilotClient() + await client.start() + + session = await client.create_session({ + "model": "gpt-5.2-codex", # Your deployment name + "provider": { + "type": "openai", + "base_url": FOUNDRY_MODEL_URL, + "wire_api": "responses", # Use "completions" for older models + "api_key": os.environ["FOUNDRY_API_KEY"], + }, + }) + + done = asyncio.Event() + + def on_event(event): + if event.type.value == "assistant.message": + print(event.data.content) + elif event.type.value == "session.idle": + done.set() + + session.on(on_event) + await session.send({"prompt": "What is 2+2?"}) + await done.wait() + + await session.destroy() + await client.stop() + +asyncio.run(main()) +``` + +
+ +
+Node.js / TypeScript + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const FOUNDRY_MODEL_URL = "https://your-resource.openai.azure.com/openai/v1/"; + +const client = new CopilotClient(); +const session = await client.createSession({ + model: "gpt-5.2-codex", // Your deployment name + provider: { + type: "openai", + baseUrl: FOUNDRY_MODEL_URL, + wireApi: "responses", // Use "completions" for older models + apiKey: process.env.FOUNDRY_API_KEY, + }, +}); + +session.on("assistant.message", (event) => { + console.log(event.data.content); +}); + +await session.sendAndWait({ prompt: "What is 2+2?" }); +await client.stop(); +``` + +
+ +
+Go + +```go +package main + +import ( + "context" + "fmt" + "os" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + client := copilot.NewClient(nil) + if err := client.Start(); err != nil { + panic(err) + } + defer client.Stop() + + session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{ + Model: "gpt-5.2-codex", // Your deployment name + Provider: &copilot.ProviderConfig{ + Type: "openai", + BaseURL: "https://your-resource.openai.azure.com/openai/v1/", + WireApi: "responses", // Use "completions" for older models + APIKey: os.Getenv("FOUNDRY_API_KEY"), + }, + }) + if err != nil { + panic(err) + } + + response, err := session.SendAndWait(copilot.MessageOptions{ + Prompt: "What is 2+2?", + }, 0) + if err != nil { + panic(err) + } + + fmt.Println(*response.Data.Content) +} +``` + +
+ +
+.NET + +```csharp +using GitHub.Copilot.SDK; + +await using var client = new CopilotClient(); +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + Model = "gpt-5.2-codex", // Your deployment name + Provider = new ProviderConfig + { + Type = "openai", + BaseUrl = "https://your-resource.openai.azure.com/openai/v1/", + WireApi = "responses", // Use "completions" for older models + ApiKey = Environment.GetEnvironmentVariable("FOUNDRY_API_KEY"), + }, +}); + +var response = await session.SendAndWaitAsync(new MessageOptions +{ + Prompt = "What is 2+2?", +}); +Console.WriteLine(response?.Data.Content); +``` + +
+ +## Provider Configuration Reference + +### ProviderConfig Fields + +| Field | Type | Description | +|-------|------|-------------| +| `type` | `"openai"` \| `"azure"` \| `"anthropic"` | Provider type (default: `"openai"`) | +| `baseUrl` / `base_url` | string | **Required.** API endpoint URL | +| `apiKey` / `api_key` | string | API key (optional for local providers like Ollama) | +| `bearerToken` / `bearer_token` | string | Bearer token auth (takes precedence over apiKey) | +| `wireApi` / `wire_api` | `"completions"` \| `"responses"` | API format (default: `"completions"`) | +| `azure.apiVersion` / `azure.api_version` | string | Azure API version (default: `"2024-10-21"`) | + +### Wire API Format + +The `wireApi` setting determines which OpenAI API format to use: + +- **`"completions"`** (default) - Chat Completions API (`/chat/completions`). Use for most models. +- **`"responses"`** - Responses API. Use for GPT-5 series models that support the newer responses format. + +### Type-Specific Notes + +**OpenAI (`type: "openai"`)** +- Works with OpenAI API and any OpenAI-compatible endpoint +- `baseUrl` should include the full path (e.g., `https://api.openai.com/v1`) + +**Azure (`type: "azure"`)** +- Use for native Azure OpenAI endpoints +- `baseUrl` should be just the host (e.g., `https://my-resource.openai.azure.com`) +- Do NOT include `/openai/v1` in the URL—the SDK handles path construction + +**Anthropic (`type: "anthropic"`)** +- For direct Anthropic API access +- Uses Claude-specific API format + +## Example Configurations + +### OpenAI Direct + +```typescript +provider: { + type: "openai", + baseUrl: "https://api.openai.com/v1", + apiKey: process.env.OPENAI_API_KEY, +} +``` + +### Azure OpenAI (Native Azure Endpoint) + +Use `type: "azure"` for endpoints at `*.openai.azure.com`: + +```typescript +provider: { + type: "azure", + baseUrl: "https://my-resource.openai.azure.com", // Just the host + apiKey: process.env.AZURE_OPENAI_KEY, + azure: { + apiVersion: "2024-10-21", + }, +} +``` + +### Azure AI Foundry (OpenAI-Compatible Endpoint) + +For Azure AI Foundry deployments with `/openai/v1/` endpoints, use `type: "openai"`: + +```typescript +provider: { + type: "openai", + baseUrl: "https://your-resource.openai.azure.com/openai/v1/", + apiKey: process.env.FOUNDRY_API_KEY, + wireApi: "responses", // For GPT-5 series models +} +``` + +### Ollama (Local) + +```typescript +provider: { + type: "openai", + baseUrl: "http://localhost:11434/v1", + // No apiKey needed for local Ollama +} +``` + +### Anthropic + +```typescript +provider: { + type: "anthropic", + baseUrl: "https://api.anthropic.com", + apiKey: process.env.ANTHROPIC_API_KEY, +} +``` + +### Bearer Token Authentication + +Some providers require bearer token authentication instead of API keys: + +```typescript +provider: { + type: "openai", + baseUrl: "https://my-custom-endpoint.example.com/v1", + bearerToken: process.env.MY_BEARER_TOKEN, // Sets Authorization header +} +``` + +## Limitations + +When using BYOK, be aware of these limitations: + +### Identity Limitations + +BYOK authentication is **key-based only**. The following identity providers are NOT supported: + +- ❌ **Microsoft Entra ID (Azure AD)** - No support for Entra managed identities or service principals +- ❌ **Third-party identity providers** - No OIDC, SAML, or other federated identity +- ❌ **Managed identities** - Azure Managed Identity is not supported + +You must use an API key or bearer token that you manage yourself. + +### Feature Limitations + +Some Copilot features may behave differently with BYOK: + +- **Model availability** - Only models supported by your provider are available +- **Rate limiting** - Subject to your provider's rate limits, not Copilot's +- **Usage tracking** - Usage is tracked by your provider, not GitHub Copilot +- **Premium requests** - Do not count against Copilot premium request quotas + +### Provider-Specific Limitations + +| Provider | Limitations | +|----------|-------------| +| Azure AI Foundry | No Entra ID auth; must use API keys | +| Ollama | No API key; local only; model support varies | +| OpenAI | Subject to OpenAI rate limits and quotas | + +## Troubleshooting + +### "Model not specified" Error + +When using BYOK, the `model` parameter is **required**: + +```typescript +// ❌ Error: Model required with custom provider +const session = await client.createSession({ + provider: { type: "openai", baseUrl: "..." }, +}); + +// ✅ Correct: Model specified +const session = await client.createSession({ + model: "gpt-4", // Required! + provider: { type: "openai", baseUrl: "..." }, +}); +``` + +### Azure Endpoint Type Confusion + +For Azure OpenAI endpoints (`*.openai.azure.com`), use the correct type: + +```typescript +// ❌ Wrong: Using "openai" type with native Azure endpoint +provider: { + type: "openai", // This won't work correctly + baseUrl: "https://my-resource.openai.azure.com", +} + +// ✅ Correct: Using "azure" type +provider: { + type: "azure", + baseUrl: "https://my-resource.openai.azure.com", +} +``` + +However, if your Azure AI Foundry deployment provides an OpenAI-compatible endpoint path (e.g., `/openai/v1/`), use `type: "openai"`: + +```typescript +// ✅ Correct: OpenAI-compatible Azure AI Foundry endpoint +provider: { + type: "openai", + baseUrl: "https://your-resource.openai.azure.com/openai/v1/", +} +``` + +### Connection Refused (Ollama) + +Ensure Ollama is running and accessible: + +```bash +# Check Ollama is running +curl http://localhost:11434/v1/models + +# Start Ollama if not running +ollama serve +``` + +### Authentication Failed + +1. Verify your API key is correct and not expired +2. Check the `baseUrl` matches your provider's expected format +3. For bearer tokens, ensure the full token is provided (not just a prefix) + +## Next Steps + +- [Authentication Overview](./index.md) - Learn about all authentication methods +- [Getting Started Guide](../getting-started.md) - Build your first Copilot-powered app diff --git a/docs/auth/index.md b/docs/auth/index.md new file mode 100644 index 00000000..ffd47625 --- /dev/null +++ b/docs/auth/index.md @@ -0,0 +1,289 @@ +# Authentication + +The GitHub Copilot SDK supports multiple authentication methods to fit different use cases. Choose the method that best matches your deployment scenario. + +## Authentication Methods + +| Method | Use Case | Copilot Subscription Required | +|--------|----------|-------------------------------| +| [GitHub Signed-in User](#github-signed-in-user) | Interactive apps where users sign in with GitHub | Yes | +| [OAuth GitHub App](#oauth-github-app) | Apps acting on behalf of users via OAuth | Yes | +| [Environment Variables](#environment-variables) | CI/CD, automation, server-to-server | Yes | +| [BYOK (Bring Your Own Key)](./byok.md) | Using your own API keys (Azure AI Foundry, OpenAI, etc.) | No | + +## GitHub Signed-in User + +This is the default authentication method when running the Copilot CLI interactively. Users authenticate via GitHub OAuth device flow, and the SDK uses their stored credentials. + +**How it works:** +1. User runs `copilot` CLI and signs in via GitHub OAuth +2. Credentials are stored securely in the system keychain +3. SDK automatically uses stored credentials + +**SDK Configuration:** + +
+Node.js / TypeScript + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +// Default: uses logged-in user credentials +const client = new CopilotClient(); +``` + +
+ +
+Python + +```python +from copilot import CopilotClient + +# Default: uses logged-in user credentials +client = CopilotClient() +await client.start() +``` + +
+ +
+Go + +```go +import copilot "github.com/github/copilot-sdk/go" + +// Default: uses logged-in user credentials +client := copilot.NewClient(nil) +``` + +
+ +
+.NET + +```csharp +using GitHub.Copilot.SDK; + +// Default: uses logged-in user credentials +await using var client = new CopilotClient(); +``` + +
+ +**When to use:** +- Desktop applications where users interact directly +- Development and testing environments +- Any scenario where a user can sign in interactively + +## OAuth GitHub App + +Use an OAuth GitHub App to authenticate users through your application and pass their credentials to the SDK. This enables your application to make Copilot API requests on behalf of users who authorize your app. + +**How it works:** +1. User authorizes your OAuth GitHub App +2. Your app receives a user access token (`gho_` or `ghu_` prefix) +3. Pass the token to the SDK via `githubToken` option + +**SDK Configuration:** + +
+Node.js / TypeScript + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient({ + githubToken: userAccessToken, // Token from OAuth flow + useLoggedInUser: false, // Don't use stored CLI credentials +}); +``` + +
+ +
+Python + +```python +from copilot import CopilotClient + +client = CopilotClient({ + "github_token": user_access_token, # Token from OAuth flow + "use_logged_in_user": False, # Don't use stored CLI credentials +}) +await client.start() +``` + +
+ +
+Go + +```go +import copilot "github.com/github/copilot-sdk/go" + +client := copilot.NewClient(&copilot.ClientOptions{ + GithubToken: userAccessToken, // Token from OAuth flow + UseLoggedInUser: copilot.Bool(false), // Don't use stored CLI credentials +}) +``` + +
+ +
+.NET + +```csharp +using GitHub.Copilot.SDK; + +await using var client = new CopilotClient(new CopilotClientOptions +{ + GithubToken = userAccessToken, // Token from OAuth flow + UseLoggedInUser = false, // Don't use stored CLI credentials +}); +``` + +
+ +**Supported token types:** +- `gho_` - OAuth user access tokens +- `ghu_` - GitHub App user access tokens +- `github_pat_` - Fine-grained personal access tokens + +**Not supported:** +- `ghp_` - Classic personal access tokens (deprecated) + +**When to use:** +- Web applications where users sign in via GitHub +- SaaS applications building on top of Copilot +- Any multi-user application where you need to make requests on behalf of different users + +## Environment Variables + +For automation, CI/CD pipelines, and server-to-server scenarios, you can authenticate using environment variables. + +**Supported environment variables (in priority order):** +1. `COPILOT_GITHUB_TOKEN` - Recommended for explicit Copilot usage +2. `GH_TOKEN` - GitHub CLI compatible +3. `GITHUB_TOKEN` - GitHub Actions compatible + +**How it works:** +1. Set one of the supported environment variables with a valid token +2. The SDK automatically detects and uses the token + +**SDK Configuration:** + +No code changes needed—the SDK automatically detects environment variables: + +
+Node.js / TypeScript + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +// Token is read from environment variable automatically +const client = new CopilotClient(); +``` + +
+ +
+Python + +```python +from copilot import CopilotClient + +# Token is read from environment variable automatically +client = CopilotClient() +await client.start() +``` + +
+ +**When to use:** +- CI/CD pipelines (GitHub Actions, Jenkins, etc.) +- Automated testing +- Server-side applications with service accounts +- Development when you don't want to use interactive login + +## BYOK (Bring Your Own Key) + +BYOK allows you to use your own API keys from model providers like Azure AI Foundry, OpenAI, or Anthropic. This bypasses GitHub Copilot authentication entirely. + +**Key benefits:** +- No GitHub Copilot subscription required +- Use enterprise model deployments +- Direct billing with your model provider +- Support for Azure AI Foundry, OpenAI, Anthropic, and OpenAI-compatible endpoints + +**See the [BYOK documentation](./byok.md) for complete details**, including: +- Azure AI Foundry setup +- Provider configuration options +- Limitations and considerations +- Complete code examples + +## Authentication Priority + +When multiple authentication methods are available, the SDK uses them in this priority order: + +1. **Explicit `githubToken`** - Token passed directly to SDK constructor +2. **HMAC key** - `CAPI_HMAC_KEY` or `COPILOT_HMAC_KEY` environment variables +3. **Direct API token** - `GITHUB_COPILOT_API_TOKEN` with `COPILOT_API_URL` +4. **Environment variable tokens** - `COPILOT_GITHUB_TOKEN` → `GH_TOKEN` → `GITHUB_TOKEN` +5. **Stored OAuth credentials** - From previous `copilot` CLI login +6. **GitHub CLI** - `gh auth` credentials + +## Disabling Auto-Login + +To prevent the SDK from automatically using stored credentials or `gh` CLI auth, use the `useLoggedInUser: false` option: + +
+Node.js / TypeScript + +```typescript +const client = new CopilotClient({ + useLoggedInUser: false, // Only use explicit tokens +}); +``` + +
+ +
+Python + +```python +client = CopilotClient({ + "use_logged_in_user": False, # Only use explicit tokens +}) +``` + +
+ +
+Go + +```go +client := copilot.NewClient(&copilot.ClientOptions{ + UseLoggedInUser: copilot.Bool(false), // Only use explicit tokens +}) +``` + +
+ +
+.NET + +```csharp +await using var client = new CopilotClient(new CopilotClientOptions +{ + UseLoggedInUser = false, // Only use explicit tokens +}); +``` + +
+ +## Next Steps + +- [BYOK Documentation](./byok.md) - Learn how to use your own API keys +- [Getting Started Guide](../getting-started.md) - Build your first Copilot-powered app +- [MCP Servers](../mcp) - Connect to external tools diff --git a/docs/compatibility.md b/docs/compatibility.md new file mode 100644 index 00000000..bc8f54cd --- /dev/null +++ b/docs/compatibility.md @@ -0,0 +1,194 @@ +# SDK and CLI Compatibility + +This document outlines which Copilot CLI features are available through the SDK and which are CLI-only. + +## Overview + +The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must be explicitly exposed through this protocol to be available in the SDK. Many interactive CLI features are terminal-specific and not available programmatically. + +## Feature Comparison + +### ✅ Available in SDK + +| Feature | SDK Method | Notes | +|---------|------------|-------| +| **Session Management** | | | +| Create session | `createSession()` | Full config support | +| Resume session | `resumeSession()` | With infinite session workspaces | +| Destroy session | `destroy()` | Clean up resources | +| Delete session | `deleteSession()` | Remove from storage | +| List sessions | `listSessions()` | All stored sessions | +| Get last session | `getLastSessionId()` | For quick resume | +| **Messaging** | | | +| Send message | `send()` | With attachments | +| Send and wait | `sendAndWait()` | Blocks until complete | +| Get history | `getMessages()` | All session events | +| Abort | `abort()` | Cancel in-flight request | +| **Tools** | | | +| Register custom tools | `registerTools()` | Full JSON Schema support | +| Tool permission control | `onPreToolUse` hook | Allow/deny/ask | +| Tool result modification | `onPostToolUse` hook | Transform results | +| Available/excluded tools | `availableTools`, `excludedTools` config | Filter tools | +| **Models** | | | +| List models | `listModels()` | With capabilities | +| Set model | `model` in session config | Per-session | +| Reasoning effort | `reasoningEffort` config | For supported models | +| **Authentication** | | | +| Get auth status | `getAuthStatus()` | Check login state | +| Use token | `githubToken` option | Programmatic auth | +| **MCP Servers** | | | +| Local/stdio servers | `mcpServers` config | Spawn processes | +| Remote HTTP/SSE | `mcpServers` config | Connect to services | +| **Hooks** | | | +| Pre-tool use | `onPreToolUse` | Permission, modify args | +| Post-tool use | `onPostToolUse` | Modify results | +| User prompt | `onUserPromptSubmitted` | Modify prompts | +| Session start/end | `onSessionStart`, `onSessionEnd` | Lifecycle | +| Error handling | `onErrorOccurred` | Custom handling | +| **Events** | | | +| All session events | `on()`, `once()` | 40+ event types | +| Streaming | `streaming: true` | Delta events | +| **Advanced** | | | +| Custom agents | `customAgents` config | Load agent definitions | +| System message | `systemMessage` config | Append or replace | +| Custom provider | `provider` config | BYOK support | +| Infinite sessions | `infiniteSessions` config | Auto-compaction | +| Permission handler | `onPermissionRequest` | Approve/deny requests | +| User input handler | `onUserInputRequest` | Handle ask_user | +| Skills | `skillDirectories` config | Custom skills | + +### ❌ Not Available in SDK (CLI-Only) + +| Feature | CLI Command/Option | Reason | +|---------|-------------------|--------| +| **Session Export** | | | +| Export to file | `--share`, `/share` | Not in protocol | +| Export to gist | `--share-gist`, `/share gist` | Not in protocol | +| **Interactive UI** | | | +| Slash commands | `/help`, `/clear`, etc. | TUI-only | +| Agent picker dialog | `/agent` | Interactive UI | +| Diff mode dialog | `/diff` | Interactive UI | +| Feedback dialog | `/feedback` | Interactive UI | +| Theme picker | `/theme` | Terminal UI | +| **Terminal Features** | | | +| Color output | `--no-color` | Terminal-specific | +| Screen reader mode | `--screen-reader` | Accessibility | +| Rich diff rendering | `--plain-diff` | Terminal rendering | +| Startup banner | `--banner` | Visual element | +| **Path/Permission Shortcuts** | | | +| Allow all paths | `--allow-all-paths` | Use permission handler | +| Allow all URLs | `--allow-all-urls` | Use permission handler | +| YOLO mode | `--yolo` | Use permission handler | +| **Directory Management** | | | +| Add directory | `/add-dir`, `--add-dir` | Configure in session | +| List directories | `/list-dirs` | TUI command | +| Change directory | `/cwd` | TUI command | +| **Plugin/MCP Management** | | | +| Plugin commands | `/plugin` | Interactive management | +| MCP server management | `/mcp` | Interactive UI | +| **Account Management** | | | +| Login flow | `/login`, `copilot auth login` | OAuth device flow | +| Logout | `/logout`, `copilot auth logout` | Direct CLI | +| User info | `/user` | TUI command | +| **Session Operations** | | | +| Clear conversation | `/clear` | TUI-only | +| Compact context | `/compact` | Use `infiniteSessions` config | +| Plan view | `/plan` | TUI-only | +| **Usage & Stats** | | | +| Token usage | `/usage` | Subscribe to usage events | +| **Code Review** | | | +| Review changes | `/review` | TUI command | +| **Delegation** | | | +| Delegate to PR | `/delegate` | TUI workflow | +| **Terminal Setup** | | | +| Shell integration | `/terminal-setup` | Shell-specific | +| **Experimental** | | | +| Toggle experimental | `/experimental` | Runtime flag | + +## Workarounds + +### Session Export + +The `--share` option is not available via SDK. Workarounds: + +1. **Collect events manually** - Subscribe to session events and build your own export: + ```typescript + const events: SessionEvent[] = []; + session.on((event) => events.push(event)); + // ... after conversation ... + const messages = await session.getMessages(); + // Format as markdown yourself + ``` + +2. **Use CLI directly for export** - Run the CLI with `--share` for one-off exports. + +### Permission Control + +Instead of `--allow-all-paths` or `--yolo`, use the permission handler: + +```typescript +const session = await client.createSession({ + onPermissionRequest: async (request) => { + // Auto-approve everything (equivalent to --yolo) + return { approved: true }; + + // Or implement custom logic + if (request.kind === "shell") { + return { approved: request.command.startsWith("git") }; + } + return { approved: true }; + }, +}); +``` + +### Token Usage Tracking + +Instead of `/usage`, subscribe to usage events: + +```typescript +session.on("assistant.usage", (event) => { + console.log("Tokens used:", { + input: event.data.inputTokens, + output: event.data.outputTokens, + }); +}); +``` + +### Context Compaction + +Instead of `/compact`, configure automatic compaction: + +```typescript +const session = await client.createSession({ + infiniteSessions: { + enabled: true, + backgroundCompactionThreshold: 0.80, // Start background compaction at 80% context utilization + bufferExhaustionThreshold: 0.95, // Block and compact at 95% context utilization + }, +}); +``` + +> **Note:** Thresholds are context utilization ratios (0.0-1.0), not absolute token counts. + +## Protocol Limitations + +The SDK can only access features exposed through the CLI's JSON-RPC protocol. If you need a CLI feature that's not available: + +1. **Check for alternatives** - Many features have SDK equivalents (see workarounds above) +2. **Use the CLI directly** - For one-off operations, invoke the CLI +3. **Request the feature** - Open an issue to request protocol support + +## Version Compatibility + +| SDK Version | CLI Version | Protocol Version | +|-------------|-------------|------------------| +| Check `package.json` | `copilot --version` | `getStatus().protocolVersion` | + +The SDK and CLI must have compatible protocol versions. The SDK will log warnings if versions are mismatched. + +## See Also + +- [Getting Started Guide](./getting-started.md) +- [Hooks Documentation](./hooks/overview.md) +- [MCP Servers Guide](./mcp/overview.md) +- [Debugging Guide](./debugging.md) diff --git a/docs/debugging.md b/docs/debugging.md new file mode 100644 index 00000000..893d2c05 --- /dev/null +++ b/docs/debugging.md @@ -0,0 +1,496 @@ +# Debugging Guide + +This guide covers common issues and debugging techniques for the Copilot SDK across all supported languages. + +## Table of Contents + +- [Enable Debug Logging](#enable-debug-logging) +- [Common Issues](#common-issues) +- [MCP Server Debugging](#mcp-server-debugging) +- [Connection Issues](#connection-issues) +- [Tool Execution Issues](#tool-execution-issues) +- [Platform-Specific Issues](#platform-specific-issues) + +--- + +## Enable Debug Logging + +The first step in debugging is enabling verbose logging to see what's happening under the hood. + +
+Node.js / TypeScript + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient({ + logLevel: "debug", // Options: "none", "error", "warning", "info", "debug", "all" +}); +``` + +
+ +
+Python + +```python +from copilot import CopilotClient + +client = CopilotClient(log_level="debug") +``` + +
+ +
+Go + +```go +import copilot "github.com/github/copilot-sdk/go" + +client, err := copilot.NewClient(copilot.ClientOptions{ + LogLevel: "debug", +}) +``` + +
+ +
+.NET + +```csharp +using GitHub.Copilot.SDK; +using Microsoft.Extensions.Logging; + +// Using ILogger +var loggerFactory = LoggerFactory.Create(builder => +{ + builder.SetMinimumLevel(LogLevel.Debug); + builder.AddConsole(); +}); + +var client = new CopilotClient(new CopilotClientOptions +{ + LogLevel = "debug", + Logger = loggerFactory.CreateLogger() +}); +``` + +
+ +### Log Directory + +The CLI writes logs to a directory. You can specify a custom location: + +
+Node.js / TypeScript + +```typescript +const client = new CopilotClient({ + cliArgs: ["--log-dir", "/path/to/logs"], +}); +``` + +
+ +
+Python + +```python +# The Python SDK does not currently support passing extra CLI arguments. +# Logs are written to the default location or can be configured via +# the CLI when running in server mode. +``` + +> **Note:** Python SDK logging configuration is limited. For advanced logging, run the CLI manually with `--log-dir` and connect via `cli_url`. + +
+ +
+Go + +```go +// The Go SDK does not currently support passing extra CLI arguments. +// For custom log directories, run the CLI manually with --log-dir +// and connect via CLIUrl option. +``` + +
+ +
+.NET + +```csharp +var client = new CopilotClient(new CopilotClientOptions +{ + CliArgs = new[] { "--log-dir", "/path/to/logs" } +}); +``` + +
+ +--- + +## Common Issues + +### "CLI not found" / "copilot: command not found" + +**Cause:** The Copilot CLI is not installed or not in PATH. + +**Solution:** + +1. Install the CLI: [Installation guide](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli) + +2. Verify installation: + ```bash + copilot --version + ``` + +3. Or specify the full path: + +
+ Node.js + + ```typescript + const client = new CopilotClient({ + cliPath: "/usr/local/bin/copilot", + }); + ``` +
+ +
+ Python + + ```python + client = CopilotClient(cli_path="/usr/local/bin/copilot") + ``` +
+ +
+ Go + + ```go + client, _ := copilot.NewClient(copilot.ClientOptions{ + CLIPath: "/usr/local/bin/copilot", + }) + ``` +
+ +
+ .NET + + ```csharp + var client = new CopilotClient(new CopilotClientOptions + { + CliPath = "/usr/local/bin/copilot" + }); + ``` +
+ +### "Not authenticated" + +**Cause:** The CLI is not authenticated with GitHub. + +**Solution:** + +1. Authenticate the CLI: + ```bash + copilot auth login + ``` + +2. Or provide a token programmatically: + +
+ Node.js + + ```typescript + const client = new CopilotClient({ + githubToken: process.env.GITHUB_TOKEN, + }); + ``` +
+ +
+ Python + + ```python + import os + client = CopilotClient(github_token=os.environ.get("GITHUB_TOKEN")) + ``` +
+ +
+ Go + + ```go + client, _ := copilot.NewClient(copilot.ClientOptions{ + GithubToken: os.Getenv("GITHUB_TOKEN"), + }) + ``` +
+ +
+ .NET + + ```csharp + var client = new CopilotClient(new CopilotClientOptions + { + GithubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN") + }); + ``` +
+ +### "Session not found" + +**Cause:** Attempting to use a session that was destroyed or doesn't exist. + +**Solution:** + +1. Ensure you're not calling methods after `destroy()`: + ```typescript + await session.destroy(); + // Don't use session after this! + ``` + +2. For resuming sessions, verify the session ID exists: + ```typescript + const sessions = await client.listSessions(); + console.log("Available sessions:", sessions); + ``` + +### "Connection refused" / "ECONNREFUSED" + +**Cause:** The CLI server process crashed or failed to start. + +**Solution:** + +1. Check if the CLI runs correctly standalone: + ```bash + copilot --server --stdio + ``` + +2. Enable auto-restart (enabled by default): + ```typescript + const client = new CopilotClient({ + autoRestart: true, + }); + ``` + +3. Check for port conflicts if using TCP mode: + ```typescript + const client = new CopilotClient({ + useStdio: false, + port: 0, // Use random available port + }); + ``` + +--- + +## MCP Server Debugging + +MCP (Model Context Protocol) servers can be tricky to debug. For comprehensive MCP debugging guidance, see the dedicated **[MCP Debugging Guide](./mcp/debugging.md)**. + +### Quick MCP Checklist + +- [ ] MCP server executable exists and runs independently +- [ ] Command path is correct (use absolute paths) +- [ ] Tools are enabled: `tools: ["*"]` +- [ ] Server responds to `initialize` request correctly +- [ ] Working directory (`cwd`) is set if needed + +### Test Your MCP Server + +Before integrating with the SDK, verify your MCP server works: + +```bash +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | /path/to/your/mcp-server +``` + +See [MCP Debugging Guide](./mcp/debugging.md) for detailed troubleshooting. + +--- + +## Connection Issues + +### Stdio vs TCP Mode + +The SDK supports two transport modes: + +| Mode | Description | Use Case | +|------|-------------|----------| +| **Stdio** (default) | CLI runs as subprocess, communicates via pipes | Local development, single process | +| **TCP** | CLI runs separately, communicates via TCP socket | Multiple clients, remote CLI | + +**Stdio mode (default):** +```typescript +const client = new CopilotClient({ + useStdio: true, // This is the default +}); +``` + +**TCP mode:** +```typescript +const client = new CopilotClient({ + useStdio: false, + port: 8080, // Or 0 for random port +}); +``` + +**Connect to existing server:** +```typescript +const client = new CopilotClient({ + cliUrl: "localhost:8080", // Connect to running server +}); +``` + +### Diagnosing Connection Failures + +1. **Check client state:** + ```typescript + console.log("Connection state:", client.getState()); + // Should be "connected" after start() + ``` + +2. **Listen for state changes:** + ```typescript + client.on("stateChange", (state) => { + console.log("State changed to:", state); + }); + ``` + +3. **Verify CLI process is running:** + ```bash + # Check for copilot processes + ps aux | grep copilot + ``` + +--- + +## Tool Execution Issues + +### Custom Tool Not Being Called + +1. **Verify tool registration:** + ```typescript + const session = await client.createSession({ + tools: [myTool], + }); + + // Check registered tools + console.log("Registered tools:", session.getTools?.()); + ``` + +2. **Check tool schema is valid JSON Schema:** + ```typescript + const myTool = { + name: "get_weather", + description: "Get weather for a location", + parameters: { + type: "object", + properties: { + location: { type: "string", description: "City name" }, + }, + required: ["location"], + }, + handler: async (args) => { + return { temperature: 72 }; + }, + }; + ``` + +3. **Ensure handler returns valid result:** + ```typescript + handler: async (args) => { + // Must return something JSON-serializable + return { success: true, data: "result" }; + + // Don't return undefined or non-serializable objects + } + ``` + +### Tool Errors Not Surfacing + +Subscribe to error events: + +```typescript +session.on("tool.execution_error", (event) => { + console.error("Tool error:", event.data); +}); + +session.on("error", (event) => { + console.error("Session error:", event.data); +}); +``` + +--- + +## Platform-Specific Issues + +### Windows + +1. **Path separators:** Use raw strings or forward slashes: + ```csharp + CliPath = @"C:\Program Files\GitHub\copilot.exe" + // or + CliPath = "C:/Program Files/GitHub/copilot.exe" + ``` + +2. **PATHEXT resolution:** The SDK handles this automatically, but if issues persist: + ```csharp + // Explicitly specify .exe + Command = "myserver.exe" // Not just "myserver" + ``` + +3. **Console encoding:** Ensure UTF-8 for proper JSON handling: + ```csharp + Console.OutputEncoding = System.Text.Encoding.UTF8; + ``` + +### macOS + +1. **Gatekeeper issues:** If CLI is blocked: + ```bash + xattr -d com.apple.quarantine /path/to/copilot + ``` + +2. **PATH issues in GUI apps:** GUI applications may not inherit shell PATH: + ```typescript + const client = new CopilotClient({ + cliPath: "/opt/homebrew/bin/copilot", // Full path + }); + ``` + +### Linux + +1. **Permission issues:** + ```bash + chmod +x /path/to/copilot + ``` + +2. **Missing libraries:** Check for required shared libraries: + ```bash + ldd /path/to/copilot + ``` + +--- + +## Getting Help + +If you're still stuck: + +1. **Collect debug information:** + - SDK version + - CLI version (`copilot --version`) + - Operating system + - Debug logs + - Minimal reproduction code + +2. **Search existing issues:** [GitHub Issues](https://github.com/github/copilot-sdk/issues) + +3. **Open a new issue** with the collected information + +## See Also + +- [Getting Started Guide](./getting-started.md) +- [MCP Overview](./mcp/overview.md) - MCP configuration and setup +- [MCP Debugging Guide](./mcp/debugging.md) - Detailed MCP troubleshooting +- [API Reference](https://github.com/github/copilot-sdk) diff --git a/docs/getting-started.md b/docs/getting-started.md index 47fb36a1..1a730905 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1155,11 +1155,13 @@ await using var session = await client.CreateSessionAsync(); ## Learn More +- [Authentication Guide](./auth/index.md) - GitHub OAuth, environment variables, and BYOK +- [BYOK (Bring Your Own Key)](./auth/byok.md) - Use your own API keys from Azure AI Foundry, OpenAI, etc. - [Node.js SDK Reference](../nodejs/README.md) - [Python SDK Reference](../python/README.md) - [Go SDK Reference](../go/README.md) - [.NET SDK Reference](../dotnet/README.md) -- [Using MCP Servers](./mcp.md) - Integrate external tools via Model Context Protocol +- [Using MCP Servers](./mcp) - Integrate external tools via Model Context Protocol - [GitHub MCP Server Documentation](https://github.com/github/github-mcp-server) - [MCP Servers Directory](https://github.com/modelcontextprotocol/servers) - Explore more MCP servers diff --git a/docs/guides/session-persistence.md b/docs/guides/session-persistence.md new file mode 100644 index 00000000..865031cc --- /dev/null +++ b/docs/guides/session-persistence.md @@ -0,0 +1,495 @@ +# Session Resume & Persistence + +This guide walks you through the SDK's session persistence capabilities—how to pause work, resume it later, and manage sessions in production environments. + +## How Sessions Work + +When you create a session, the Copilot CLI maintains conversation history, tool state, and planning context. By default, this state lives in memory and disappears when the session ends. With persistence enabled, you can resume sessions across restarts, container migrations, or even different client instances. + +```mermaid +flowchart LR + A[🆕 Create] --> B[⚡ Active] --> C[💾 Paused] --> D[🔄 Resume] + D --> B +``` + +| State | What happens | +|-------|--------------| +| **Create** | `session_id` assigned | +| **Active** | Send prompts, tool calls, responses | +| **Paused** | State saved to disk | +| **Resume** | State loaded from disk | + +## Quick Start: Creating a Resumable Session + +The key to resumable sessions is providing your own `session_id`. Without one, the SDK generates a random ID and the session can't be resumed later. + +### TypeScript + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient(); + +// Create a session with a meaningful ID +const session = await client.createSession({ + sessionId: "user-123-task-456", + model: "gpt-5.2-codex", +}); + +// Do some work... +await session.sendPrompt({ content: "Analyze my codebase" }); + +// Session state is automatically persisted +// You can safely close the client +``` + +### Python + +```python +from copilot import CopilotClient + +client = CopilotClient() + +# Create a session with a meaningful ID +session = await client.create_session( + session_id="user-123-task-456", + model="gpt-5.2-codex", +) + +# Do some work... +await session.send_prompt(content="Analyze my codebase") + +# Session state is automatically persisted +``` + +### Go + +```go +client, _ := copilot.NewClient() + +// Create a session with a meaningful ID +session, _ := client.CreateSession(copilot.CreateSessionOptions{ + SessionID: "user-123-task-456", + Model: "gpt-5.2-codex", +}) + +// Do some work... +session.SendPrompt(copilot.PromptOptions{Content: "Analyze my codebase"}) + +// Session state is automatically persisted +``` + +### C# (.NET) + +```csharp +using GitHub.Copilot.SDK; + +var client = new CopilotClient(); + +// Create a session with a meaningful ID +var session = await client.CreateSessionAsync(new CreateSessionOptions +{ + SessionId = "user-123-task-456", + Model = "gpt-5.2-codex", +}); + +// Do some work... +await session.SendPromptAsync(new PromptOptions { Content = "Analyze my codebase" }); + +// Session state is automatically persisted +``` + +## Resuming a Session + +Later—minutes, hours, or even days—you can resume the session from where you left off. + +```mermaid +flowchart LR + subgraph Day1["Day 1"] + A1[Client A:
createSession] --> A2[Work...] + end + + A2 --> S[(💾 Storage:
~/.copilot/session-state/)] + S --> B1 + + subgraph Day2["Day 2"] + B1[Client B:
resumeSession] --> B2[Continue] + end +``` + +### TypeScript + +```typescript +// Resume from a different client instance (or after restart) +const session = await client.resumeSession("user-123-task-456"); + +// Continue where you left off +await session.sendPrompt({ content: "What did we discuss earlier?" }); +``` + +### Python + +```python +# Resume from a different client instance (or after restart) +session = await client.resume_session("user-123-task-456") + +# Continue where you left off +await session.send_prompt(content="What did we discuss earlier?") +``` + +### Go + +```go +// Resume from a different client instance (or after restart) +session, _ := client.ResumeSession("user-123-task-456", copilot.ResumeSessionOptions{}) + +// Continue where you left off +session.SendPrompt(copilot.PromptOptions{Content: "What did we discuss earlier?"}) +``` + +### C# (.NET) + +```csharp +// Resume from a different client instance (or after restart) +var session = await client.ResumeSessionAsync("user-123-task-456"); + +// Continue where you left off +await session.SendPromptAsync(new PromptOptions { Content = "What did we discuss earlier?" }); +``` + +## Using BYOK (Bring Your Own Key) with Resumed Sessions + +When using your own API keys, you must re-provide the provider configuration when resuming. API keys are never persisted to disk for security reasons. + +```typescript +// Original session with BYOK +const session = await client.createSession({ + sessionId: "user-123-task-456", + model: "gpt-5.2-codex", + provider: { + type: "azure", + endpoint: "https://my-resource.openai.azure.com", + apiKey: process.env.AZURE_OPENAI_KEY, + deploymentId: "my-gpt-deployment", + }, +}); + +// When resuming, you MUST re-provide the provider config +const resumed = await client.resumeSession("user-123-task-456", { + provider: { + type: "azure", + endpoint: "https://my-resource.openai.azure.com", + apiKey: process.env.AZURE_OPENAI_KEY, // Required again + deploymentId: "my-gpt-deployment", + }, +}); +``` + +## What Gets Persisted? + +Session state is saved to `~/.copilot/session-state/{sessionId}/`: + +``` +~/.copilot/session-state/ +└── user-123-task-456/ + ├── checkpoints/ # Conversation history snapshots + │ ├── 001.json # Initial state + │ ├── 002.json # After first interaction + │ └── ... # Incremental checkpoints + ├── plan.md # Agent's planning state (if any) + └── files/ # Session artifacts + ├── analysis.md # Files the agent created + └── notes.txt # Working documents +``` + +| Data | Persisted? | Notes | +|------|------------|-------| +| Conversation history | ✅ Yes | Full message thread | +| Tool call results | ✅ Yes | Cached for context | +| Agent planning state | ✅ Yes | `plan.md` file | +| Session artifacts | ✅ Yes | In `files/` directory | +| Provider/API keys | ❌ No | Security: must re-provide | +| In-memory tool state | ❌ No | Tools should be stateless | + +## Session ID Best Practices + +Choose session IDs that encode ownership and purpose. This makes auditing and cleanup much easier. + +| Pattern | Example | Use Case | +|---------|---------|----------| +| ❌ `abc123` | Random IDs | Hard to audit, no ownership info | +| ✅ `user-{userId}-{taskId}` | `user-alice-pr-review-42` | Multi-user apps | +| ✅ `tenant-{tenantId}-{workflow}` | `tenant-acme-onboarding` | Multi-tenant SaaS | +| ✅ `{userId}-{taskId}-{timestamp}` | `alice-deploy-1706932800` | Time-based cleanup | + +**Benefits of structured IDs:** +- Easy to audit: "Show all sessions for user alice" +- Easy to clean up: "Delete all sessions older than X" +- Natural access control: Parse user ID from session ID + +### Example: Generating Session IDs + +```typescript +function createSessionId(userId: string, taskType: string): string { + const timestamp = Date.now(); + return `${userId}-${taskType}-${timestamp}`; +} + +const sessionId = createSessionId("alice", "code-review"); +// → "alice-code-review-1706932800000" +``` + +```python +import time + +def create_session_id(user_id: str, task_type: str) -> str: + timestamp = int(time.time()) + return f"{user_id}-{task_type}-{timestamp}" + +session_id = create_session_id("alice", "code-review") +# → "alice-code-review-1706932800" +``` + +## Managing Session Lifecycle + +### Listing Active Sessions + +```typescript +const sessions = await client.listSessions(); +console.log(`Found ${sessions.length} sessions`); + +for (const session of sessions) { + console.log(`- ${session.sessionId} (created: ${session.createdAt})`); +} +``` + +### Cleaning Up Old Sessions + +```typescript +async function cleanupExpiredSessions(maxAgeMs: number) { + const sessions = await client.listSessions(); + const now = Date.now(); + + for (const session of sessions) { + const age = now - new Date(session.createdAt).getTime(); + if (age > maxAgeMs) { + await client.deleteSession(session.sessionId); + console.log(`Deleted expired session: ${session.sessionId}`); + } + } +} + +// Clean up sessions older than 24 hours +await cleanupExpiredSessions(24 * 60 * 60 * 1000); +``` + +### Explicit Session Destruction + +When a task completes, destroy the session explicitly rather than waiting for timeouts: + +```typescript +try { + // Do work... + await session.sendPrompt({ content: "Complete the task" }); + + // Task complete - clean up + await session.destroy(); +} catch (error) { + // Clean up even on error + await session.destroy(); + throw error; +} +``` + +## Automatic Cleanup: Idle Timeout + +The CLI has a built-in 30-minute idle timeout. Sessions without activity are automatically cleaned up: + +```mermaid +flowchart LR + A["⚡ Last Activity"] --> B["⏳ 25 min
timeout_warning"] --> C["🧹 30 min
destroyed"] +``` + +Listen for idle events to know when work completes: + +```typescript +session.on("session.idle", (event) => { + console.log(`Session idle for ${event.idleDurationMs}ms`); +}); +``` + +## Deployment Patterns + +### Pattern 1: One CLI Server Per User (Recommended) + +Best for: Strong isolation, multi-tenant environments, Azure Dynamic Sessions. + +```mermaid +flowchart LR + subgraph Users[" "] + UA[User A] --> CA[CLI A] + UB[User B] --> CB[CLI B] + UC[User C] --> CC[CLI C] + end + CA --> SA[(Storage A)] + CB --> SB[(Storage B)] + CC --> SC[(Storage C)] +``` + +**Benefits:** ✅ Complete isolation | ✅ Simple security | ✅ Easy scaling + +### Pattern 2: Shared CLI Server (Resource Efficient) + +Best for: Internal tools, trusted environments, resource-constrained setups. + +```mermaid +flowchart LR + UA[User A] --> CLI + UB[User B] --> CLI + UC[User C] --> CLI + CLI[🖥️ Shared CLI] --> SA[Session A] + CLI --> SB[Session B] + CLI --> SC[Session C] +``` + +**Requirements:** +- ⚠️ Unique session IDs per user +- ⚠️ Application-level access control +- ⚠️ Session ID validation before operations + +```typescript +// Application-level access control for shared CLI +async function resumeSessionWithAuth( + client: CopilotClient, + sessionId: string, + currentUserId: string +): Promise { + // Parse user from session ID + const [sessionUserId] = sessionId.split("-"); + + if (sessionUserId !== currentUserId) { + throw new Error("Access denied: session belongs to another user"); + } + + return client.resumeSession(sessionId); +} +``` + +## Azure Dynamic Sessions + +For serverless/container deployments where containers can restart or migrate: + +### Mount Persistent Storage + +The session state directory must be mounted to persistent storage: + +```yaml +# Azure Container Instance example +containers: + - name: copilot-agent + image: my-agent:latest + volumeMounts: + - name: session-storage + mountPath: /home/app/.copilot/session-state + +volumes: + - name: session-storage + azureFile: + shareName: copilot-sessions + storageAccountName: myaccount +``` + +```mermaid +flowchart LR + subgraph Before["Container A"] + CLI1[CLI + Session X] + end + + CLI1 --> |persist| Azure[(☁️ Azure File Share)] + Azure --> |restore| CLI2 + + subgraph After["Container B (restart)"] + CLI2[CLI + Session X] + end +``` + +**Session survives container restarts!** + +## Infinite Sessions for Long-Running Workflows + +For workflows that might exceed context limits, enable infinite sessions with automatic compaction: + +```typescript +const session = await client.createSession({ + sessionId: "long-workflow-123", + infiniteSessions: { + enabled: true, + backgroundCompactionThreshold: 0.80, // Start compaction at 80% context + bufferExhaustionThreshold: 0.95, // Block at 95% if needed + }, +}); +``` + +> **Note:** Thresholds are context utilization ratios (0.0-1.0), not absolute token counts. See the [Compatibility Guide](../compatibility.md) for details. + +## Limitations & Considerations + +| Limitation | Description | Mitigation | +|------------|-------------|------------| +| **BYOK re-authentication** | API keys aren't persisted | Store keys in your secret manager; provide on resume | +| **Writable storage** | `~/.copilot/session-state/` must be writable | Mount persistent volume in containers | +| **No session locking** | Concurrent access to same session is undefined | Implement application-level locking or queue | +| **Tool state not persisted** | In-memory tool state is lost | Design tools to be stateless or persist their own state | + +### Handling Concurrent Access + +The SDK doesn't provide built-in session locking. If multiple clients might access the same session: + +```typescript +// Option 1: Application-level locking with Redis +import Redis from "ioredis"; + +const redis = new Redis(); + +async function withSessionLock( + sessionId: string, + fn: () => Promise +): Promise { + const lockKey = `session-lock:${sessionId}`; + const acquired = await redis.set(lockKey, "locked", "NX", "EX", 300); + + if (!acquired) { + throw new Error("Session is in use by another client"); + } + + try { + return await fn(); + } finally { + await redis.del(lockKey); + } +} + +// Usage +await withSessionLock("user-123-task-456", async () => { + const session = await client.resumeSession("user-123-task-456"); + await session.sendPrompt({ content: "Continue the task" }); +}); +``` + +## Summary + +| Feature | How to Use | +|---------|------------| +| **Create resumable session** | Provide your own `sessionId` | +| **Resume session** | `client.resumeSession(sessionId)` | +| **BYOK resume** | Re-provide `provider` config | +| **List sessions** | `client.listSessions()` | +| **Delete session** | `client.deleteSession(sessionId)` | +| **Destroy active session** | `session.destroy()` | +| **Containerized deployment** | Mount `~/.copilot/session-state/` to persistent storage | + +## Next Steps + +- [Hooks Overview](../hooks/overview.md) - Customize session behavior with hooks +- [Compatibility Guide](../compatibility.md) - SDK vs CLI feature comparison +- [Debugging Guide](../debugging.md) - Troubleshoot session issues diff --git a/docs/hooks/error-handling.md b/docs/hooks/error-handling.md new file mode 100644 index 00000000..8aedd32e --- /dev/null +++ b/docs/hooks/error-handling.md @@ -0,0 +1,385 @@ +# Error Handling Hook + +The `onErrorOccurred` hook is called when errors occur during session execution. Use it to: + +- Implement custom error logging +- Track error patterns +- Provide user-friendly error messages +- Trigger alerts for critical errors + +## Hook Signature + +
+Node.js / TypeScript + +```typescript +type ErrorOccurredHandler = ( + input: ErrorOccurredHookInput, + invocation: HookInvocation +) => Promise; +``` + +
+ +
+Python + +```python +ErrorOccurredHandler = Callable[ + [ErrorOccurredHookInput, HookInvocation], + Awaitable[ErrorOccurredHookOutput | None] +] +``` + +
+ +
+Go + +```go +type ErrorOccurredHandler func( + input ErrorOccurredHookInput, + invocation HookInvocation, +) (*ErrorOccurredHookOutput, error) +``` + +
+ +
+.NET + +```csharp +public delegate Task ErrorOccurredHandler( + ErrorOccurredHookInput input, + HookInvocation invocation); +``` + +
+ +## Input + +| Field | Type | Description | +|-------|------|-------------| +| `timestamp` | number | Unix timestamp when the error occurred | +| `cwd` | string | Current working directory | +| `error` | string | Error message | +| `errorContext` | string | Where the error occurred: `"model_call"`, `"tool_execution"`, `"system"`, or `"user_input"` | +| `recoverable` | boolean | Whether the error can potentially be recovered from | + +## Output + +Return `null` or `undefined` to use default error handling. Otherwise, return an object with: + +| Field | Type | Description | +|-------|------|-------------| +| `suppressOutput` | boolean | If true, don't show error output to user | +| `errorHandling` | string | How to handle: `"retry"`, `"skip"`, or `"abort"` | +| `retryCount` | number | Number of times to retry (if errorHandling is `"retry"`) | +| `userNotification` | string | Custom message to show the user | + +## Examples + +### Basic Error Logging + +
+Node.js / TypeScript + +```typescript +const session = await client.createSession({ + hooks: { + onErrorOccurred: async (input, invocation) => { + console.error(`[${invocation.sessionId}] Error: ${input.error}`); + console.error(` Context: ${input.errorContext}`); + console.error(` Recoverable: ${input.recoverable}`); + return null; + }, + }, +}); +``` + +
+ +
+Python + +```python +async def on_error_occurred(input_data, invocation): + print(f"[{invocation['session_id']}] Error: {input_data['error']}") + print(f" Context: {input_data['error_context']}") + print(f" Recoverable: {input_data['recoverable']}") + return None + +session = await client.create_session({ + "hooks": {"on_error_occurred": on_error_occurred} +}) +``` + +
+ +
+Go + +```go +session, _ := client.CreateSession(ctx, copilot.SessionConfig{ + Hooks: &copilot.SessionHooks{ + OnErrorOccurred: func(input copilot.ErrorOccurredHookInput, inv copilot.HookInvocation) (*copilot.ErrorOccurredHookOutput, error) { + fmt.Printf("[%s] Error: %s\n", inv.SessionID, input.Error) + fmt.Printf(" Context: %s\n", input.ErrorContext) + fmt.Printf(" Recoverable: %v\n", input.Recoverable) + return nil, nil + }, + }, +}) +``` + +
+ +
+.NET + +```csharp +var session = await client.CreateSessionAsync(new SessionConfig +{ + Hooks = new SessionHooks + { + OnErrorOccurred = (input, invocation) => + { + Console.Error.WriteLine($"[{invocation.SessionId}] Error: {input.Error}"); + Console.Error.WriteLine($" Type: {input.ErrorType}"); + if (!string.IsNullOrEmpty(input.Stack)) + { + Console.Error.WriteLine($" Stack: {input.Stack}"); + } + return Task.FromResult(null); + }, + }, +}); +``` + +
+ +### Send Errors to Monitoring Service + +```typescript +import { captureException } from "@sentry/node"; // or your monitoring service + +const session = await client.createSession({ + hooks: { + onErrorOccurred: async (input, invocation) => { + captureException(new Error(input.error), { + tags: { + sessionId: invocation.sessionId, + errorType: input.errorType, + }, + extra: { + stack: input.stack, + context: input.context, + cwd: input.cwd, + }, + }); + + return null; + }, + }, +}); +``` + +### User-Friendly Error Messages + +```typescript +const ERROR_MESSAGES: Record = { + "rate_limit": "Too many requests. Please wait a moment and try again.", + "auth_failed": "Authentication failed. Please check your credentials.", + "network_error": "Network connection issue. Please check your internet connection.", + "timeout": "Request timed out. Try breaking your request into smaller parts.", +}; + +const session = await client.createSession({ + hooks: { + onErrorOccurred: async (input) => { + const friendlyMessage = ERROR_MESSAGES[input.errorType]; + + if (friendlyMessage) { + return { + modifiedMessage: friendlyMessage, + }; + } + + return null; + }, + }, +}); +``` + +### Suppress Non-Critical Errors + +```typescript +const SUPPRESSED_ERRORS = [ + "tool_not_found", + "file_not_found", +]; + +const session = await client.createSession({ + hooks: { + onErrorOccurred: async (input) => { + if (SUPPRESSED_ERRORS.includes(input.errorType)) { + console.log(`Suppressed error: ${input.errorType}`); + return { suppressError: true }; + } + return null; + }, + }, +}); +``` + +### Add Recovery Context + +```typescript +const session = await client.createSession({ + hooks: { + onErrorOccurred: async (input) => { + if (input.errorType === "tool_execution_failed") { + return { + additionalContext: ` +The tool failed. Here are some recovery suggestions: +- Check if required dependencies are installed +- Verify file paths are correct +- Try a simpler approach + `.trim(), + }; + } + + if (input.errorType === "rate_limit") { + return { + additionalContext: "Rate limit hit. Waiting before retry.", + }; + } + + return null; + }, + }, +}); +``` + +### Track Error Patterns + +```typescript +interface ErrorStats { + count: number; + lastOccurred: number; + contexts: string[]; +} + +const errorStats = new Map(); + +const session = await client.createSession({ + hooks: { + onErrorOccurred: async (input, invocation) => { + const key = `${input.errorType}:${input.error.substring(0, 50)}`; + + const existing = errorStats.get(key) || { + count: 0, + lastOccurred: 0, + contexts: [], + }; + + existing.count++; + existing.lastOccurred = input.timestamp; + existing.contexts.push(invocation.sessionId); + + errorStats.set(key, existing); + + // Alert if error is recurring + if (existing.count >= 5) { + console.warn(`Recurring error detected: ${key} (${existing.count} times)`); + } + + return null; + }, + }, +}); +``` + +### Alert on Critical Errors + +```typescript +const CRITICAL_ERRORS = ["auth_failed", "api_error", "system_error"]; + +const session = await client.createSession({ + hooks: { + onErrorOccurred: async (input, invocation) => { + if (CRITICAL_ERRORS.includes(input.errorType)) { + await sendAlert({ + level: "critical", + message: `Critical error in session ${invocation.sessionId}`, + error: input.error, + type: input.errorType, + timestamp: new Date(input.timestamp).toISOString(), + }); + } + + return null; + }, + }, +}); +``` + +### Combine with Other Hooks for Context + +```typescript +const sessionContext = new Map(); + +const session = await client.createSession({ + hooks: { + onPreToolUse: async (input, invocation) => { + const ctx = sessionContext.get(invocation.sessionId) || {}; + ctx.lastTool = input.toolName; + sessionContext.set(invocation.sessionId, ctx); + return { permissionDecision: "allow" }; + }, + + onUserPromptSubmitted: async (input, invocation) => { + const ctx = sessionContext.get(invocation.sessionId) || {}; + ctx.lastPrompt = input.prompt.substring(0, 100); + sessionContext.set(invocation.sessionId, ctx); + return null; + }, + + onErrorOccurred: async (input, invocation) => { + const ctx = sessionContext.get(invocation.sessionId); + + console.error(`Error in session ${invocation.sessionId}:`); + console.error(` Error: ${input.error}`); + console.error(` Type: ${input.errorType}`); + if (ctx?.lastTool) { + console.error(` Last tool: ${ctx.lastTool}`); + } + if (ctx?.lastPrompt) { + console.error(` Last prompt: ${ctx.lastPrompt}...`); + } + + return null; + }, + }, +}); +``` + +## Best Practices + +1. **Always log errors** - Even if you suppress them from users, keep logs for debugging. + +2. **Categorize errors** - Use `errorType` to handle different errors appropriately. + +3. **Don't swallow critical errors** - Only suppress errors you're certain are non-critical. + +4. **Keep hooks fast** - Error handling shouldn't slow down recovery. + +5. **Provide helpful context** - When errors occur, `additionalContext` can help the model recover. + +6. **Monitor error patterns** - Track recurring errors to identify systemic issues. + +## See Also + +- [Hooks Overview](./overview.md) +- [Session Lifecycle Hooks](./session-lifecycle.md) +- [Debugging Guide](../debugging.md) diff --git a/docs/hooks/overview.md b/docs/hooks/overview.md new file mode 100644 index 00000000..0d365846 --- /dev/null +++ b/docs/hooks/overview.md @@ -0,0 +1,233 @@ +# Session Hooks + +Hooks allow you to intercept and customize the behavior of Copilot sessions at key points in the conversation lifecycle. Use hooks to: + +- **Control tool execution** - approve, deny, or modify tool calls +- **Transform results** - modify tool outputs before they're processed +- **Add context** - inject additional information at session start +- **Handle errors** - implement custom error handling +- **Audit and log** - track all interactions for compliance + +## Available Hooks + +| Hook | Trigger | Use Case | +|------|---------|----------| +| [`onPreToolUse`](./pre-tool-use.md) | Before a tool executes | Permission control, argument validation | +| [`onPostToolUse`](./post-tool-use.md) | After a tool executes | Result transformation, logging | +| [`onUserPromptSubmitted`](./user-prompt-submitted.md) | When user sends a message | Prompt modification, filtering | +| [`onSessionStart`](./session-lifecycle.md#session-start) | Session begins | Add context, configure session | +| [`onSessionEnd`](./session-lifecycle.md#session-end) | Session ends | Cleanup, analytics | +| [`onErrorOccurred`](./error-handling.md) | Error happens | Custom error handling | + +## Quick Start + +
+Node.js / TypeScript + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient(); + +const session = await client.createSession({ + hooks: { + onPreToolUse: async (input) => { + console.log(`Tool called: ${input.toolName}`); + // Allow all tools + return { permissionDecision: "allow" }; + }, + onPostToolUse: async (input) => { + console.log(`Tool result: ${JSON.stringify(input.toolResult)}`); + return null; // No modifications + }, + onSessionStart: async (input) => { + return { additionalContext: "User prefers concise answers." }; + }, + }, +}); +``` + +
+ +
+Python + +```python +from copilot import CopilotClient + +async def main(): + client = CopilotClient() + + async def on_pre_tool_use(input_data, invocation): + print(f"Tool called: {input_data['toolName']}") + return {"permissionDecision": "allow"} + + async def on_post_tool_use(input_data, invocation): + print(f"Tool result: {input_data['toolResult']}") + return None + + async def on_session_start(input_data, invocation): + return {"additionalContext": "User prefers concise answers."} + + session = await client.create_session({ + "hooks": { + "on_pre_tool_use": on_pre_tool_use, + "on_post_tool_use": on_post_tool_use, + "on_session_start": on_session_start, + } + }) +``` + +
+ +
+Go + +```go +package main + +import ( + "fmt" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + client, _ := copilot.NewClient(copilot.ClientOptions{}) + + session, _ := client.CreateSession(ctx, copilot.SessionConfig{ + Hooks: &copilot.SessionHooks{ + OnPreToolUse: func(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) { + fmt.Printf("Tool called: %s\n", input.ToolName) + return &copilot.PreToolUseHookOutput{ + PermissionDecision: "allow", + }, nil + }, + OnPostToolUse: func(input copilot.PostToolUseHookInput, inv copilot.HookInvocation) (*copilot.PostToolUseHookOutput, error) { + fmt.Printf("Tool result: %v\n", input.ToolResult) + return nil, nil + }, + OnSessionStart: func(input copilot.SessionStartHookInput, inv copilot.HookInvocation) (*copilot.SessionStartHookOutput, error) { + return &copilot.SessionStartHookOutput{ + AdditionalContext: "User prefers concise answers.", + }, nil + }, + }, + }) +} +``` + +
+ +
+.NET + +```csharp +using GitHub.Copilot.SDK; + +var client = new CopilotClient(); + +var session = await client.CreateSessionAsync(new SessionConfig +{ + Hooks = new SessionHooks + { + OnPreToolUse = (input, invocation) => + { + Console.WriteLine($"Tool called: {input.ToolName}"); + return Task.FromResult( + new PreToolUseHookOutput { PermissionDecision = "allow" } + ); + }, + OnPostToolUse = (input, invocation) => + { + Console.WriteLine($"Tool result: {input.ToolResult}"); + return Task.FromResult(null); + }, + OnSessionStart = (input, invocation) => + { + return Task.FromResult( + new SessionStartHookOutput { AdditionalContext = "User prefers concise answers." } + ); + }, + }, +}); +``` + +
+ +## Hook Invocation Context + +Every hook receives an `invocation` parameter with context about the current session: + +| Field | Type | Description | +|-------|------|-------------| +| `sessionId` | string | The ID of the current session | + +This allows hooks to maintain state or perform session-specific logic. + +## Common Patterns + +### Logging All Tool Calls + +```typescript +const session = await client.createSession({ + hooks: { + onPreToolUse: async (input) => { + console.log(`[${new Date().toISOString()}] Tool: ${input.toolName}, Args: ${JSON.stringify(input.toolArgs)}`); + return { permissionDecision: "allow" }; + }, + onPostToolUse: async (input) => { + console.log(`[${new Date().toISOString()}] Result: ${JSON.stringify(input.toolResult)}`); + return null; + }, + }, +}); +``` + +### Blocking Dangerous Tools + +```typescript +const BLOCKED_TOOLS = ["shell", "bash", "exec"]; + +const session = await client.createSession({ + hooks: { + onPreToolUse: async (input) => { + if (BLOCKED_TOOLS.includes(input.toolName)) { + return { + permissionDecision: "deny", + permissionDecisionReason: "Shell access is not permitted", + }; + } + return { permissionDecision: "allow" }; + }, + }, +}); +``` + +### Adding User Context + +```typescript +const session = await client.createSession({ + hooks: { + onSessionStart: async () => { + const userPrefs = await loadUserPreferences(); + return { + additionalContext: `User preferences: ${JSON.stringify(userPrefs)}`, + }; + }, + }, +}); +``` + +## Hook Guides + +- **[Pre-Tool Use Hook](./pre-tool-use.md)** - Control tool execution permissions +- **[Post-Tool Use Hook](./post-tool-use.md)** - Transform tool results +- **[User Prompt Submitted Hook](./user-prompt-submitted.md)** - Modify user prompts +- **[Session Lifecycle Hooks](./session-lifecycle.md)** - Session start and end +- **[Error Handling Hook](./error-handling.md)** - Custom error handling + +## See Also + +- [Getting Started Guide](../getting-started.md) +- [Custom Tools](../getting-started.md#step-4-add-custom-tools) +- [Debugging Guide](../debugging.md) diff --git a/docs/hooks/post-tool-use.md b/docs/hooks/post-tool-use.md new file mode 100644 index 00000000..24ee7ebe --- /dev/null +++ b/docs/hooks/post-tool-use.md @@ -0,0 +1,337 @@ +# Post-Tool Use Hook + +The `onPostToolUse` hook is called **after** a tool executes. Use it to: + +- Transform or filter tool results +- Log tool execution for auditing +- Add context based on results +- Suppress results from the conversation + +## Hook Signature + +
+Node.js / TypeScript + +```typescript +type PostToolUseHandler = ( + input: PostToolUseHookInput, + invocation: HookInvocation +) => Promise; +``` + +
+ +
+Python + +```python +PostToolUseHandler = Callable[ + [PostToolUseHookInput, HookInvocation], + Awaitable[PostToolUseHookOutput | None] +] +``` + +
+ +
+Go + +```go +type PostToolUseHandler func( + input PostToolUseHookInput, + invocation HookInvocation, +) (*PostToolUseHookOutput, error) +``` + +
+ +
+.NET + +```csharp +public delegate Task PostToolUseHandler( + PostToolUseHookInput input, + HookInvocation invocation); +``` + +
+ +## Input + +| Field | Type | Description | +|-------|------|-------------| +| `timestamp` | number | Unix timestamp when the hook was triggered | +| `cwd` | string | Current working directory | +| `toolName` | string | Name of the tool that was called | +| `toolArgs` | object | Arguments that were passed to the tool | +| `toolResult` | object | Result returned by the tool | + +## Output + +Return `null` or `undefined` to pass through the result unchanged. Otherwise, return an object with any of these fields: + +| Field | Type | Description | +|-------|------|-------------| +| `modifiedResult` | object | Modified result to use instead of original | +| `additionalContext` | string | Extra context injected into the conversation | +| `suppressOutput` | boolean | If true, result won't appear in conversation | + +## Examples + +### Log All Tool Results + +
+Node.js / TypeScript + +```typescript +const session = await client.createSession({ + hooks: { + onPostToolUse: async (input, invocation) => { + console.log(`[${invocation.sessionId}] Tool: ${input.toolName}`); + console.log(` Args: ${JSON.stringify(input.toolArgs)}`); + console.log(` Result: ${JSON.stringify(input.toolResult)}`); + return null; // Pass through unchanged + }, + }, +}); +``` + +
+ +
+Python + +```python +async def on_post_tool_use(input_data, invocation): + print(f"[{invocation['session_id']}] Tool: {input_data['toolName']}") + print(f" Args: {input_data['toolArgs']}") + print(f" Result: {input_data['toolResult']}") + return None # Pass through unchanged + +session = await client.create_session({ + "hooks": {"on_post_tool_use": on_post_tool_use} +}) +``` + +
+ +
+Go + +```go +session, _ := client.CreateSession(ctx, copilot.SessionConfig{ + Hooks: &copilot.SessionHooks{ + OnPostToolUse: func(input copilot.PostToolUseHookInput, inv copilot.HookInvocation) (*copilot.PostToolUseHookOutput, error) { + fmt.Printf("[%s] Tool: %s\n", inv.SessionID, input.ToolName) + fmt.Printf(" Args: %v\n", input.ToolArgs) + fmt.Printf(" Result: %v\n", input.ToolResult) + return nil, nil + }, + }, +}) +``` + +
+ +
+.NET + +```csharp +var session = await client.CreateSessionAsync(new SessionConfig +{ + Hooks = new SessionHooks + { + OnPostToolUse = (input, invocation) => + { + Console.WriteLine($"[{invocation.SessionId}] Tool: {input.ToolName}"); + Console.WriteLine($" Args: {input.ToolArgs}"); + Console.WriteLine($" Result: {input.ToolResult}"); + return Task.FromResult(null); + }, + }, +}); +``` + +
+ +### Redact Sensitive Data + +```typescript +const SENSITIVE_PATTERNS = [ + /api[_-]?key["\s:=]+["']?[\w-]+["']?/gi, + /password["\s:=]+["']?[\w-]+["']?/gi, + /secret["\s:=]+["']?[\w-]+["']?/gi, +]; + +const session = await client.createSession({ + hooks: { + onPostToolUse: async (input) => { + if (typeof input.toolResult === "string") { + let redacted = input.toolResult; + for (const pattern of SENSITIVE_PATTERNS) { + redacted = redacted.replace(pattern, "[REDACTED]"); + } + + if (redacted !== input.toolResult) { + return { modifiedResult: redacted }; + } + } + return null; + }, + }, +}); +``` + +### Truncate Large Results + +```typescript +const MAX_RESULT_LENGTH = 10000; + +const session = await client.createSession({ + hooks: { + onPostToolUse: async (input) => { + const resultStr = JSON.stringify(input.toolResult); + + if (resultStr.length > MAX_RESULT_LENGTH) { + return { + modifiedResult: { + truncated: true, + originalLength: resultStr.length, + content: resultStr.substring(0, MAX_RESULT_LENGTH) + "...", + }, + additionalContext: `Note: Result was truncated from ${resultStr.length} to ${MAX_RESULT_LENGTH} characters.`, + }; + } + return null; + }, + }, +}); +``` + +### Add Context Based on Results + +```typescript +const session = await client.createSession({ + hooks: { + onPostToolUse: async (input) => { + // If a file read returned an error, add helpful context + if (input.toolName === "read_file" && input.toolResult?.error) { + return { + additionalContext: "Tip: If the file doesn't exist, consider creating it or checking the path.", + }; + } + + // If shell command failed, add debugging hint + if (input.toolName === "shell" && input.toolResult?.exitCode !== 0) { + return { + additionalContext: "The command failed. Check if required dependencies are installed.", + }; + } + + return null; + }, + }, +}); +``` + +### Filter Error Stack Traces + +```typescript +const session = await client.createSession({ + hooks: { + onPostToolUse: async (input) => { + if (input.toolResult?.error && input.toolResult?.stack) { + // Remove internal stack trace details + return { + modifiedResult: { + error: input.toolResult.error, + // Keep only first 3 lines of stack + stack: input.toolResult.stack.split("\n").slice(0, 3).join("\n"), + }, + }; + } + return null; + }, + }, +}); +``` + +### Audit Trail for Compliance + +```typescript +interface AuditEntry { + timestamp: number; + sessionId: string; + toolName: string; + args: unknown; + result: unknown; + success: boolean; +} + +const auditLog: AuditEntry[] = []; + +const session = await client.createSession({ + hooks: { + onPostToolUse: async (input, invocation) => { + auditLog.push({ + timestamp: input.timestamp, + sessionId: invocation.sessionId, + toolName: input.toolName, + args: input.toolArgs, + result: input.toolResult, + success: !input.toolResult?.error, + }); + + // Optionally persist to database/file + await saveAuditLog(auditLog); + + return null; + }, + }, +}); +``` + +### Suppress Noisy Results + +```typescript +const NOISY_TOOLS = ["list_directory", "search_codebase"]; + +const session = await client.createSession({ + hooks: { + onPostToolUse: async (input) => { + if (NOISY_TOOLS.includes(input.toolName)) { + // Summarize instead of showing full result + const items = Array.isArray(input.toolResult) + ? input.toolResult + : input.toolResult?.items || []; + + return { + modifiedResult: { + summary: `Found ${items.length} items`, + firstFew: items.slice(0, 5), + }, + }; + } + return null; + }, + }, +}); +``` + +## Best Practices + +1. **Return `null` when no changes needed** - This is more efficient than returning an empty object or the same result. + +2. **Be careful with result modification** - Changing results can affect how the model interprets tool output. Only modify when necessary. + +3. **Use `additionalContext` for hints** - Instead of modifying results, add context to help the model interpret them. + +4. **Consider privacy when logging** - Tool results may contain sensitive data. Apply redaction before logging. + +5. **Keep hooks fast** - Post-tool hooks run synchronously. Heavy processing should be done asynchronously or batched. + +## See Also + +- [Hooks Overview](./overview.md) +- [Pre-Tool Use Hook](./pre-tool-use.md) +- [Error Handling Hook](./error-handling.md) diff --git a/docs/hooks/pre-tool-use.md b/docs/hooks/pre-tool-use.md new file mode 100644 index 00000000..37a6c3d0 --- /dev/null +++ b/docs/hooks/pre-tool-use.md @@ -0,0 +1,293 @@ +# Pre-Tool Use Hook + +The `onPreToolUse` hook is called **before** a tool executes. Use it to: + +- Approve or deny tool execution +- Modify tool arguments +- Add context for the tool +- Suppress tool output from the conversation + +## Hook Signature + +
+Node.js / TypeScript + +```typescript +type PreToolUseHandler = ( + input: PreToolUseHookInput, + invocation: HookInvocation +) => Promise; +``` + +
+ +
+Python + +```python +PreToolUseHandler = Callable[ + [PreToolUseHookInput, HookInvocation], + Awaitable[PreToolUseHookOutput | None] +] +``` + +
+ +
+Go + +```go +type PreToolUseHandler func( + input PreToolUseHookInput, + invocation HookInvocation, +) (*PreToolUseHookOutput, error) +``` + +
+ +
+.NET + +```csharp +public delegate Task PreToolUseHandler( + PreToolUseHookInput input, + HookInvocation invocation); +``` + +
+ +## Input + +| Field | Type | Description | +|-------|------|-------------| +| `timestamp` | number | Unix timestamp when the hook was triggered | +| `cwd` | string | Current working directory | +| `toolName` | string | Name of the tool being called | +| `toolArgs` | object | Arguments passed to the tool | + +## Output + +Return `null` or `undefined` to allow the tool to execute with no changes. Otherwise, return an object with any of these fields: + +| Field | Type | Description | +|-------|------|-------------| +| `permissionDecision` | `"allow"` \| `"deny"` \| `"ask"` | Whether to allow the tool call | +| `permissionDecisionReason` | string | Explanation shown to user (for deny/ask) | +| `modifiedArgs` | object | Modified arguments to pass to the tool | +| `additionalContext` | string | Extra context injected into the conversation | +| `suppressOutput` | boolean | If true, tool output won't appear in conversation | + +### Permission Decisions + +| Decision | Behavior | +|----------|----------| +| `"allow"` | Tool executes normally | +| `"deny"` | Tool is blocked, reason shown to user | +| `"ask"` | User is prompted to approve (interactive mode) | + +## Examples + +### Allow All Tools (Logging Only) + +
+Node.js / TypeScript + +```typescript +const session = await client.createSession({ + hooks: { + onPreToolUse: async (input, invocation) => { + console.log(`[${invocation.sessionId}] Calling ${input.toolName}`); + console.log(` Args: ${JSON.stringify(input.toolArgs)}`); + return { permissionDecision: "allow" }; + }, + }, +}); +``` + +
+ +
+Python + +```python +async def on_pre_tool_use(input_data, invocation): + print(f"[{invocation['session_id']}] Calling {input_data['toolName']}") + print(f" Args: {input_data['toolArgs']}") + return {"permissionDecision": "allow"} + +session = await client.create_session({ + "hooks": {"on_pre_tool_use": on_pre_tool_use} +}) +``` + +
+ +
+Go + +```go +session, _ := client.CreateSession(ctx, copilot.SessionConfig{ + Hooks: &copilot.SessionHooks{ + OnPreToolUse: func(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) { + fmt.Printf("[%s] Calling %s\n", inv.SessionID, input.ToolName) + fmt.Printf(" Args: %v\n", input.ToolArgs) + return &copilot.PreToolUseHookOutput{ + PermissionDecision: "allow", + }, nil + }, + }, +}) +``` + +
+ +
+.NET + +```csharp +var session = await client.CreateSessionAsync(new SessionConfig +{ + Hooks = new SessionHooks + { + OnPreToolUse = (input, invocation) => + { + Console.WriteLine($"[{invocation.SessionId}] Calling {input.ToolName}"); + Console.WriteLine($" Args: {input.ToolArgs}"); + return Task.FromResult( + new PreToolUseHookOutput { PermissionDecision = "allow" } + ); + }, + }, +}); +``` + +
+ +### Block Specific Tools + +```typescript +const BLOCKED_TOOLS = ["shell", "bash", "write_file", "delete_file"]; + +const session = await client.createSession({ + hooks: { + onPreToolUse: async (input) => { + if (BLOCKED_TOOLS.includes(input.toolName)) { + return { + permissionDecision: "deny", + permissionDecisionReason: `Tool '${input.toolName}' is not permitted in this environment`, + }; + } + return { permissionDecision: "allow" }; + }, + }, +}); +``` + +### Modify Tool Arguments + +```typescript +const session = await client.createSession({ + hooks: { + onPreToolUse: async (input) => { + // Add a default timeout to all shell commands + if (input.toolName === "shell" && input.toolArgs) { + const args = input.toolArgs as { command: string; timeout?: number }; + return { + permissionDecision: "allow", + modifiedArgs: { + ...args, + timeout: args.timeout ?? 30000, // Default 30s timeout + }, + }; + } + return { permissionDecision: "allow" }; + }, + }, +}); +``` + +### Restrict File Access to Specific Directories + +```typescript +const ALLOWED_DIRECTORIES = ["/home/user/projects", "/tmp"]; + +const session = await client.createSession({ + hooks: { + onPreToolUse: async (input) => { + if (input.toolName === "read_file" || input.toolName === "write_file") { + const args = input.toolArgs as { path: string }; + const isAllowed = ALLOWED_DIRECTORIES.some(dir => + args.path.startsWith(dir) + ); + + if (!isAllowed) { + return { + permissionDecision: "deny", + permissionDecisionReason: `Access to '${args.path}' is not permitted. Allowed directories: ${ALLOWED_DIRECTORIES.join(", ")}`, + }; + } + } + return { permissionDecision: "allow" }; + }, + }, +}); +``` + +### Suppress Verbose Tool Output + +```typescript +const VERBOSE_TOOLS = ["list_directory", "search_files"]; + +const session = await client.createSession({ + hooks: { + onPreToolUse: async (input) => { + return { + permissionDecision: "allow", + suppressOutput: VERBOSE_TOOLS.includes(input.toolName), + }; + }, + }, +}); +``` + +### Add Context Based on Tool + +```typescript +const session = await client.createSession({ + hooks: { + onPreToolUse: async (input) => { + if (input.toolName === "query_database") { + return { + permissionDecision: "allow", + additionalContext: "Remember: This database uses PostgreSQL syntax. Always use parameterized queries.", + }; + } + return { permissionDecision: "allow" }; + }, + }, +}); +``` + +## Best Practices + +1. **Always return a decision** - Returning `null` allows the tool, but being explicit with `{ permissionDecision: "allow" }` is clearer. + +2. **Provide helpful denial reasons** - When denying, explain why so users understand: + ```typescript + return { + permissionDecision: "deny", + permissionDecisionReason: "Shell commands require approval. Please describe what you want to accomplish.", + }; + ``` + +3. **Be careful with argument modification** - Ensure modified args maintain the expected schema for the tool. + +4. **Consider performance** - Pre-tool hooks run synchronously before each tool call. Keep them fast. + +5. **Use `suppressOutput` judiciously** - Suppressing output means the model won't see the result, which may affect conversation quality. + +## See Also + +- [Hooks Overview](./overview.md) +- [Post-Tool Use Hook](./post-tool-use.md) +- [Debugging Guide](../debugging.md) diff --git a/docs/hooks/session-lifecycle.md b/docs/hooks/session-lifecycle.md new file mode 100644 index 00000000..a1187cba --- /dev/null +++ b/docs/hooks/session-lifecycle.md @@ -0,0 +1,441 @@ +# Session Lifecycle Hooks + +Session lifecycle hooks let you respond to session start and end events. Use them to: + +- Initialize context when sessions begin +- Clean up resources when sessions end +- Track session metrics and analytics +- Configure session behavior dynamically + +## Session Start Hook {#session-start} + +The `onSessionStart` hook is called when a session begins (new or resumed). + +### Hook Signature + +
+Node.js / TypeScript + +```typescript +type SessionStartHandler = ( + input: SessionStartHookInput, + invocation: HookInvocation +) => Promise; +``` + +
+ +
+Python + +```python +SessionStartHandler = Callable[ + [SessionStartHookInput, HookInvocation], + Awaitable[SessionStartHookOutput | None] +] +``` + +
+ +
+Go + +```go +type SessionStartHandler func( + input SessionStartHookInput, + invocation HookInvocation, +) (*SessionStartHookOutput, error) +``` + +
+ +
+.NET + +```csharp +public delegate Task SessionStartHandler( + SessionStartHookInput input, + HookInvocation invocation); +``` + +
+ +### Input + +| Field | Type | Description | +|-------|------|-------------| +| `timestamp` | number | Unix timestamp when the hook was triggered | +| `cwd` | string | Current working directory | +| `source` | `"startup"` \| `"resume"` \| `"new"` | How the session was started | +| `initialPrompt` | string \| undefined | The initial prompt if provided | + +### Output + +| Field | Type | Description | +|-------|------|-------------| +| `additionalContext` | string | Context to add at session start | +| `modifiedConfig` | object | Override session configuration | + +### Examples + +#### Add Project Context at Start + +
+Node.js / TypeScript + +```typescript +const session = await client.createSession({ + hooks: { + onSessionStart: async (input, invocation) => { + console.log(`Session ${invocation.sessionId} started (${input.source})`); + + const projectInfo = await detectProjectType(input.cwd); + + return { + additionalContext: ` +This is a ${projectInfo.type} project. +Main language: ${projectInfo.language} +Package manager: ${projectInfo.packageManager} + `.trim(), + }; + }, + }, +}); +``` + +
+ +
+Python + +```python +async def on_session_start(input_data, invocation): + print(f"Session {invocation['session_id']} started ({input_data['source']})") + + project_info = await detect_project_type(input_data["cwd"]) + + return { + "additionalContext": f""" +This is a {project_info['type']} project. +Main language: {project_info['language']} +Package manager: {project_info['packageManager']} + """.strip() + } + +session = await client.create_session({ + "hooks": {"on_session_start": on_session_start} +}) +``` + +
+ +#### Handle Session Resume + +```typescript +const session = await client.createSession({ + hooks: { + onSessionStart: async (input, invocation) => { + if (input.source === "resume") { + // Load previous session state + const previousState = await loadSessionState(invocation.sessionId); + + return { + additionalContext: ` +Session resumed. Previous context: +- Last topic: ${previousState.lastTopic} +- Open files: ${previousState.openFiles.join(", ")} + `.trim(), + }; + } + return null; + }, + }, +}); +``` + +#### Load User Preferences + +```typescript +const session = await client.createSession({ + hooks: { + onSessionStart: async () => { + const preferences = await loadUserPreferences(); + + const contextParts = []; + + if (preferences.language) { + contextParts.push(`Preferred language: ${preferences.language}`); + } + if (preferences.codeStyle) { + contextParts.push(`Code style: ${preferences.codeStyle}`); + } + if (preferences.verbosity === "concise") { + contextParts.push("Keep responses brief and to the point."); + } + + return { + additionalContext: contextParts.join("\n"), + }; + }, + }, +}); +``` + +--- + +## Session End Hook {#session-end} + +The `onSessionEnd` hook is called when a session ends. + +### Hook Signature + +
+Node.js / TypeScript + +```typescript +type SessionEndHandler = ( + input: SessionEndHookInput, + invocation: HookInvocation +) => Promise; +``` + +
+ +
+Python + +```python +SessionEndHandler = Callable[ + [SessionEndHookInput, HookInvocation], + Awaitable[SessionEndHookOutput | None] +] +``` + +
+ +
+Go + +```go +type SessionEndHandler func( + input SessionEndHookInput, + invocation HookInvocation, +) (*SessionEndHookOutput, error) +``` + +
+ +
+.NET + +```csharp +public delegate Task SessionEndHandler( + SessionEndHookInput input, + HookInvocation invocation); +``` + +
+ +### Input + +| Field | Type | Description | +|-------|------|-------------| +| `timestamp` | number | Unix timestamp when the hook was triggered | +| `cwd` | string | Current working directory | +| `reason` | string | Why the session ended (see below) | +| `finalMessage` | string \| undefined | The last message from the session | +| `error` | string \| undefined | Error message if session ended due to error | + +#### End Reasons + +| Reason | Description | +|--------|-------------| +| `"complete"` | Session completed normally | +| `"error"` | Session ended due to an error | +| `"abort"` | Session was aborted by user or code | +| `"timeout"` | Session timed out | +| `"user_exit"` | User explicitly ended the session | + +### Output + +| Field | Type | Description | +|-------|------|-------------| +| `suppressOutput` | boolean | Suppress the final session output | +| `cleanupActions` | string[] | List of cleanup actions to perform | +| `sessionSummary` | string | Summary of the session for logging/analytics | + +### Examples + +#### Track Session Metrics + +
+Node.js / TypeScript + +```typescript +const sessionStartTimes = new Map(); + +const session = await client.createSession({ + hooks: { + onSessionStart: async (input, invocation) => { + sessionStartTimes.set(invocation.sessionId, input.timestamp); + return null; + }, + onSessionEnd: async (input, invocation) => { + const startTime = sessionStartTimes.get(invocation.sessionId); + const duration = startTime ? input.timestamp - startTime : 0; + + await recordMetrics({ + sessionId: invocation.sessionId, + duration, + endReason: input.reason, + }); + + sessionStartTimes.delete(invocation.sessionId); + return null; + }, + }, +}); +``` + +
+ +
+Python + +```python +session_start_times = {} + +async def on_session_start(input_data, invocation): + session_start_times[invocation["session_id"]] = input_data["timestamp"] + return None + +async def on_session_end(input_data, invocation): + start_time = session_start_times.get(invocation["session_id"]) + duration = input_data["timestamp"] - start_time if start_time else 0 + + await record_metrics({ + "session_id": invocation["session_id"], + "duration": duration, + "end_reason": input_data["reason"], + }) + + session_start_times.pop(invocation["session_id"], None) + return None + +session = await client.create_session({ + "hooks": { + "on_session_start": on_session_start, + "on_session_end": on_session_end, + } +}) +``` + +
+ +#### Clean Up Resources + +```typescript +const sessionResources = new Map(); + +const session = await client.createSession({ + hooks: { + onSessionStart: async (input, invocation) => { + sessionResources.set(invocation.sessionId, { tempFiles: [] }); + return null; + }, + onSessionEnd: async (input, invocation) => { + const resources = sessionResources.get(invocation.sessionId); + + if (resources) { + // Clean up temp files + for (const file of resources.tempFiles) { + await fs.unlink(file).catch(() => {}); + } + sessionResources.delete(invocation.sessionId); + } + + console.log(`Session ${invocation.sessionId} ended: ${input.reason}`); + return null; + }, + }, +}); +``` + +#### Save Session State for Resume + +```typescript +const session = await client.createSession({ + hooks: { + onSessionEnd: async (input, invocation) => { + if (input.reason !== "error") { + // Save state for potential resume + await saveSessionState(invocation.sessionId, { + endTime: input.timestamp, + cwd: input.cwd, + reason: input.reason, + }); + } + return null; + }, + }, +}); +``` + +#### Log Session Summary + +```typescript +const sessionData: Record = {}; + +const session = await client.createSession({ + hooks: { + onSessionStart: async (input, invocation) => { + sessionData[invocation.sessionId] = { + prompts: 0, + tools: 0, + startTime: input.timestamp + }; + return null; + }, + onUserPromptSubmitted: async (_, invocation) => { + sessionData[invocation.sessionId].prompts++; + return null; + }, + onPreToolUse: async (_, invocation) => { + sessionData[invocation.sessionId].tools++; + return { permissionDecision: "allow" }; + }, + onSessionEnd: async (input, invocation) => { + const data = sessionData[invocation.sessionId]; + console.log(` +Session Summary: + ID: ${invocation.sessionId} + Duration: ${(input.timestamp - data.startTime) / 1000}s + Prompts: ${data.prompts} + Tool calls: ${data.tools} + End reason: ${input.reason} + `.trim()); + + delete sessionData[invocation.sessionId]; + return null; + }, + }, +}); +``` + +## Best Practices + +1. **Keep `onSessionStart` fast** - Users are waiting for the session to be ready. + +2. **Handle all end reasons** - Don't assume sessions end cleanly; handle errors and aborts. + +3. **Clean up resources** - Use `onSessionEnd` to free any resources allocated during the session. + +4. **Store minimal state** - If tracking session data, keep it lightweight. + +5. **Make cleanup idempotent** - `onSessionEnd` might not be called if the process crashes. + +## See Also + +- [Hooks Overview](./overview.md) +- [Error Handling Hook](./error-handling.md) +- [Debugging Guide](../debugging.md) diff --git a/docs/hooks/user-prompt-submitted.md b/docs/hooks/user-prompt-submitted.md new file mode 100644 index 00000000..fc2d14a7 --- /dev/null +++ b/docs/hooks/user-prompt-submitted.md @@ -0,0 +1,357 @@ +# User Prompt Submitted Hook + +The `onUserPromptSubmitted` hook is called when a user submits a message. Use it to: + +- Modify or enhance user prompts +- Add context before processing +- Filter or validate user input +- Implement prompt templates + +## Hook Signature + +
+Node.js / TypeScript + +```typescript +type UserPromptSubmittedHandler = ( + input: UserPromptSubmittedHookInput, + invocation: HookInvocation +) => Promise; +``` + +
+ +
+Python + +```python +UserPromptSubmittedHandler = Callable[ + [UserPromptSubmittedHookInput, HookInvocation], + Awaitable[UserPromptSubmittedHookOutput | None] +] +``` + +
+ +
+Go + +```go +type UserPromptSubmittedHandler func( + input UserPromptSubmittedHookInput, + invocation HookInvocation, +) (*UserPromptSubmittedHookOutput, error) +``` + +
+ +
+.NET + +```csharp +public delegate Task UserPromptSubmittedHandler( + UserPromptSubmittedHookInput input, + HookInvocation invocation); +``` + +
+ +## Input + +| Field | Type | Description | +|-------|------|-------------| +| `timestamp` | number | Unix timestamp when the hook was triggered | +| `cwd` | string | Current working directory | +| `prompt` | string | The user's submitted prompt | + +## Output + +Return `null` or `undefined` to use the prompt unchanged. Otherwise, return an object with any of these fields: + +| Field | Type | Description | +|-------|------|-------------| +| `modifiedPrompt` | string | Modified prompt to use instead of original | +| `additionalContext` | string | Extra context added to the conversation | +| `suppressOutput` | boolean | If true, suppress the assistant's response output | + +## Examples + +### Log All User Prompts + +
+Node.js / TypeScript + +```typescript +const session = await client.createSession({ + hooks: { + onUserPromptSubmitted: async (input, invocation) => { + console.log(`[${invocation.sessionId}] User: ${input.prompt}`); + return null; // Pass through unchanged + }, + }, +}); +``` + +
+ +
+Python + +```python +async def on_user_prompt_submitted(input_data, invocation): + print(f"[{invocation['session_id']}] User: {input_data['prompt']}") + return None + +session = await client.create_session({ + "hooks": {"on_user_prompt_submitted": on_user_prompt_submitted} +}) +``` + +
+ +
+Go + +```go +session, _ := client.CreateSession(ctx, copilot.SessionConfig{ + Hooks: &copilot.SessionHooks{ + OnUserPromptSubmitted: func(input copilot.UserPromptSubmittedHookInput, inv copilot.HookInvocation) (*copilot.UserPromptSubmittedHookOutput, error) { + fmt.Printf("[%s] User: %s\n", inv.SessionID, input.Prompt) + return nil, nil + }, + }, +}) +``` + +
+ +
+.NET + +```csharp +var session = await client.CreateSessionAsync(new SessionConfig +{ + Hooks = new SessionHooks + { + OnUserPromptSubmitted = (input, invocation) => + { + Console.WriteLine($"[{invocation.SessionId}] User: {input.Prompt}"); + return Task.FromResult(null); + }, + }, +}); +``` + +
+ +### Add Project Context + +```typescript +const session = await client.createSession({ + hooks: { + onUserPromptSubmitted: async (input) => { + const projectInfo = await getProjectInfo(); + + return { + additionalContext: ` +Project: ${projectInfo.name} +Language: ${projectInfo.language} +Framework: ${projectInfo.framework} + `.trim(), + }; + }, + }, +}); +``` + +### Expand Shorthand Commands + +```typescript +const SHORTCUTS: Record = { + "/fix": "Please fix the errors in the code", + "/explain": "Please explain this code in detail", + "/test": "Please write unit tests for this code", + "/refactor": "Please refactor this code to improve readability and maintainability", +}; + +const session = await client.createSession({ + hooks: { + onUserPromptSubmitted: async (input) => { + for (const [shortcut, expansion] of Object.entries(SHORTCUTS)) { + if (input.prompt.startsWith(shortcut)) { + const rest = input.prompt.slice(shortcut.length).trim(); + return { + modifiedPrompt: `${expansion}${rest ? `: ${rest}` : ""}`, + }; + } + } + return null; + }, + }, +}); +``` + +### Content Filtering + +```typescript +const BLOCKED_PATTERNS = [ + /password\s*[:=]/i, + /api[_-]?key\s*[:=]/i, + /secret\s*[:=]/i, +]; + +const session = await client.createSession({ + hooks: { + onUserPromptSubmitted: async (input) => { + for (const pattern of BLOCKED_PATTERNS) { + if (pattern.test(input.prompt)) { + return { + reject: true, + rejectReason: "Please don't include sensitive credentials in your prompts. Use environment variables instead.", + }; + } + } + return null; + }, + }, +}); +``` + +### Enforce Prompt Length Limits + +```typescript +const MAX_PROMPT_LENGTH = 10000; + +const session = await client.createSession({ + hooks: { + onUserPromptSubmitted: async (input) => { + if (input.prompt.length > MAX_PROMPT_LENGTH) { + return { + reject: true, + rejectReason: `Prompt too long (${input.prompt.length} chars). Maximum allowed: ${MAX_PROMPT_LENGTH} chars. Please shorten your request.`, + }; + } + return null; + }, + }, +}); +``` + +### Add User Preferences + +```typescript +interface UserPreferences { + codeStyle: "concise" | "verbose"; + preferredLanguage: string; + experienceLevel: "beginner" | "intermediate" | "expert"; +} + +const session = await client.createSession({ + hooks: { + onUserPromptSubmitted: async (input) => { + const prefs: UserPreferences = await loadUserPreferences(); + + const contextParts = []; + + if (prefs.codeStyle === "concise") { + contextParts.push("User prefers concise code with minimal comments."); + } else { + contextParts.push("User prefers verbose code with detailed comments."); + } + + if (prefs.experienceLevel === "beginner") { + contextParts.push("Explain concepts in simple terms."); + } + + return { + additionalContext: contextParts.join(" "), + }; + }, + }, +}); +``` + +### Rate Limiting + +```typescript +const promptTimestamps: number[] = []; +const RATE_LIMIT = 10; // prompts +const RATE_WINDOW = 60000; // 1 minute + +const session = await client.createSession({ + hooks: { + onUserPromptSubmitted: async (input) => { + const now = Date.now(); + + // Remove timestamps outside the window + while (promptTimestamps.length > 0 && promptTimestamps[0] < now - RATE_WINDOW) { + promptTimestamps.shift(); + } + + if (promptTimestamps.length >= RATE_LIMIT) { + return { + reject: true, + rejectReason: `Rate limit exceeded. Please wait before sending more prompts.`, + }; + } + + promptTimestamps.push(now); + return null; + }, + }, +}); +``` + +### Prompt Templates + +```typescript +const TEMPLATES: Record string> = { + "bug:": (desc) => `I found a bug: ${desc} + +Please help me: +1. Understand why this is happening +2. Suggest a fix +3. Explain how to prevent similar bugs`, + + "feature:": (desc) => `I want to implement this feature: ${desc} + +Please: +1. Outline the implementation approach +2. Identify potential challenges +3. Provide sample code`, +}; + +const session = await client.createSession({ + hooks: { + onUserPromptSubmitted: async (input) => { + for (const [prefix, template] of Object.entries(TEMPLATES)) { + if (input.prompt.toLowerCase().startsWith(prefix)) { + const args = input.prompt.slice(prefix.length).trim(); + return { + modifiedPrompt: template(args), + }; + } + } + return null; + }, + }, +}); +``` + +## Best Practices + +1. **Preserve user intent** - When modifying prompts, ensure the core intent remains clear. + +2. **Be transparent about modifications** - If you significantly change a prompt, consider logging or notifying the user. + +3. **Use `additionalContext` over `modifiedPrompt`** - Adding context is less intrusive than rewriting the prompt. + +4. **Provide clear rejection reasons** - When rejecting prompts, explain why and how to fix it. + +5. **Keep processing fast** - This hook runs on every user message. Avoid slow operations. + +## See Also + +- [Hooks Overview](./overview.md) +- [Session Lifecycle Hooks](./session-lifecycle.md) +- [Pre-Tool Use Hook](./pre-tool-use.md) diff --git a/docs/mcp/debugging.md b/docs/mcp/debugging.md new file mode 100644 index 00000000..34e22a19 --- /dev/null +++ b/docs/mcp/debugging.md @@ -0,0 +1,412 @@ +# MCP Server Debugging Guide + +This guide covers debugging techniques specific to MCP (Model Context Protocol) servers when using the Copilot SDK. + +## Table of Contents + +- [Quick Diagnostics](#quick-diagnostics) +- [Testing MCP Servers Independently](#testing-mcp-servers-independently) +- [Common Issues](#common-issues) +- [Platform-Specific Issues](#platform-specific-issues) +- [Advanced Debugging](#advanced-debugging) + +--- + +## Quick Diagnostics + +### Checklist + +Before diving deep, verify these basics: + +- [ ] MCP server executable exists and is runnable +- [ ] Command path is correct (use absolute paths when in doubt) +- [ ] Tools are enabled (`tools: ["*"]` or specific tool names) +- [ ] Server implements MCP protocol correctly (responds to `initialize`) +- [ ] No firewall/antivirus blocking the process (Windows) + +### Enable MCP Debug Logging + +Add environment variables to your MCP server config: + +```typescript +mcpServers: { + "my-server": { + type: "local", + command: "/path/to/server", + args: [], + env: { + MCP_DEBUG: "1", + DEBUG: "*", + NODE_DEBUG: "mcp", // For Node.js MCP servers + }, + }, +} +``` + +--- + +## Testing MCP Servers Independently + +Always test your MCP server outside the SDK first. + +### Manual Protocol Test + +Send an `initialize` request via stdin: + +```bash +# Unix/macOS +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | /path/to/your/mcp-server + +# Windows (PowerShell) +'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | C:\path\to\your\mcp-server.exe +``` + +**Expected response:** +```json +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"your-server","version":"1.0"}}} +``` + +### Test Tool Listing + +After initialization, request the tools list: + +```bash +echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | /path/to/your/mcp-server +``` + +**Expected response:** +```json +{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"my_tool","description":"Does something","inputSchema":{...}}]}} +``` + +### Interactive Testing Script + +Create a test script to interactively debug your MCP server: + +```bash +#!/bin/bash +# test-mcp.sh + +SERVER="$1" + +# Initialize +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' + +# Send initialized notification +echo '{"jsonrpc":"2.0","method":"notifications/initialized"}' + +# List tools +echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' + +# Keep stdin open +cat +``` + +Usage: +```bash +./test-mcp.sh | /path/to/mcp-server +``` + +--- + +## Common Issues + +### Server Not Starting + +**Symptoms:** No tools appear, no errors in logs. + +**Causes & Solutions:** + +| Cause | Solution | +|-------|----------| +| Wrong command path | Use absolute path: `/usr/local/bin/server` | +| Missing executable permission | Run `chmod +x /path/to/server` | +| Missing dependencies | Check with `ldd` (Linux) or run manually | +| Working directory issues | Set `cwd` in config | + +**Debug by running manually:** +```bash +# Run exactly what the SDK would run +cd /expected/working/dir +/path/to/command arg1 arg2 +``` + +### Server Starts But Tools Don't Appear + +**Symptoms:** Server process runs but no tools are available. + +**Causes & Solutions:** + +1. **Tools not enabled in config:** + ```typescript + mcpServers: { + "server": { + // ... + tools: ["*"], // Must be "*" or list of tool names + }, + } + ``` + +2. **Server doesn't expose tools:** + - Test with `tools/list` request manually + - Check server implements `tools/list` method + +3. **Initialization handshake fails:** + - Server must respond to `initialize` correctly + - Server must handle `notifications/initialized` + +### Tools Listed But Never Called + +**Symptoms:** Tools appear in debug logs but model doesn't use them. + +**Causes & Solutions:** + +1. **Prompt doesn't clearly need the tool:** + ```typescript + // Too vague + await session.sendAndWait({ prompt: "What's the weather?" }); + + // Better - explicitly mentions capability + await session.sendAndWait({ + prompt: "Use the weather tool to get the current temperature in Seattle" + }); + ``` + +2. **Tool description unclear:** + ```typescript + // Bad - model doesn't know when to use it + { name: "do_thing", description: "Does a thing" } + + // Good - clear purpose + { name: "get_weather", description: "Get current weather conditions for a city. Returns temperature, humidity, and conditions." } + ``` + +3. **Tool schema issues:** + - Ensure `inputSchema` is valid JSON Schema + - Required fields must be in `required` array + +### Timeout Errors + +**Symptoms:** `MCP tool call timed out` errors. + +**Solutions:** + +1. **Increase timeout:** + ```typescript + mcpServers: { + "slow-server": { + // ... + timeout: 300000, // 5 minutes + }, + } + ``` + +2. **Optimize server performance:** + - Add progress logging to identify bottleneck + - Consider async operations + - Check for blocking I/O + +3. **For long-running tools**, consider streaming responses if supported. + +### JSON-RPC Errors + +**Symptoms:** Parse errors, invalid request errors. + +**Common causes:** + +1. **Server writes to stdout incorrectly:** + - Debug output going to stdout instead of stderr + - Extra newlines or whitespace + + ```typescript + // Wrong - pollutes stdout + console.log("Debug info"); + + // Correct - use stderr for debug + console.error("Debug info"); + ``` + +2. **Encoding issues:** + - Ensure UTF-8 encoding + - No BOM (Byte Order Mark) + +3. **Message framing:** + - Each message must be a complete JSON object + - Newline-delimited (one message per line) + +--- + +## Platform-Specific Issues + +### Windows + +#### .NET Console Apps / Tools + +```csharp +// Correct configuration for .NET exe +["my-dotnet-server"] = new McpLocalServerConfig +{ + Type = "local", + Command = @"C:\Tools\MyServer\MyServer.exe", // Full path with .exe + Args = new List(), + Cwd = @"C:\Tools\MyServer", // Set working directory + Tools = new List { "*" }, +} + +// For dotnet tool (DLL) +["my-dotnet-tool"] = new McpLocalServerConfig +{ + Type = "local", + Command = "dotnet", + Args = new List { @"C:\Tools\MyTool\MyTool.dll" }, + Cwd = @"C:\Tools\MyTool", + Tools = new List { "*" }, +} +``` + +#### NPX Commands + +```csharp +// Windows needs cmd /c for npx +["filesystem"] = new McpLocalServerConfig +{ + Type = "local", + Command = "cmd", + Args = new List { "/c", "npx", "-y", "@modelcontextprotocol/server-filesystem", "C:\\allowed\\path" }, + Tools = new List { "*" }, +} +``` + +#### Path Issues + +- Use raw strings (`@"C:\path"`) or forward slashes (`"C:/path"`) +- Avoid spaces in paths when possible +- If spaces required, ensure proper quoting + +#### Antivirus/Firewall + +Windows Defender or other AV may block: +- New executables +- Processes communicating via stdin/stdout + +**Solution:** Add exclusions for your MCP server executable. + +### macOS + +#### Gatekeeper Blocking + +```bash +# If server is blocked +xattr -d com.apple.quarantine /path/to/mcp-server +``` + +#### Homebrew Paths + +```typescript +// GUI apps may not have /opt/homebrew in PATH +mcpServers: { + "my-server": { + command: "/opt/homebrew/bin/node", // Full path + args: ["/path/to/server.js"], + }, +} +``` + +### Linux + +#### Permission Issues + +```bash +chmod +x /path/to/mcp-server +``` + +#### Missing Shared Libraries + +```bash +# Check dependencies +ldd /path/to/mcp-server + +# Install missing libraries +apt install libfoo # Debian/Ubuntu +yum install libfoo # RHEL/CentOS +``` + +--- + +## Advanced Debugging + +### Capture All MCP Traffic + +Create a wrapper script to log all communication: + +```bash +#!/bin/bash +# mcp-debug-wrapper.sh + +LOG="/tmp/mcp-debug-$(date +%s).log" +ACTUAL_SERVER="$1" +shift + +echo "=== MCP Debug Session ===" >> "$LOG" +echo "Server: $ACTUAL_SERVER" >> "$LOG" +echo "Args: $@" >> "$LOG" +echo "=========================" >> "$LOG" + +# Tee stdin/stdout to log file +tee -a "$LOG" | "$ACTUAL_SERVER" "$@" 2>> "$LOG" | tee -a "$LOG" +``` + +Use it: +```typescript +mcpServers: { + "debug-server": { + command: "/path/to/mcp-debug-wrapper.sh", + args: ["/actual/server/path", "arg1", "arg2"], + }, +} +``` + +### Inspect with MCP Inspector + +Use the official MCP Inspector tool: + +```bash +npx @modelcontextprotocol/inspector /path/to/your/mcp-server +``` + +This provides a web UI to: +- Send test requests +- View responses +- Inspect tool schemas + +### Protocol Version Mismatches + +Check your server supports the protocol version the SDK uses: + +```json +// In initialize response, check protocolVersion +{"result":{"protocolVersion":"2024-11-05",...}} +``` + +If versions don't match, update your MCP server library. + +--- + +## Debugging Checklist + +When opening an issue or asking for help, collect: + +- [ ] SDK language and version +- [ ] CLI version (`copilot --version`) +- [ ] MCP server type (Node.js, Python, .NET, Go, etc.) +- [ ] Full MCP server configuration (redact secrets) +- [ ] Result of manual `initialize` test +- [ ] Result of manual `tools/list` test +- [ ] Debug logs from SDK +- [ ] Any error messages + +## See Also + +- [MCP Overview](./overview.md) - Configuration and setup +- [General Debugging Guide](../debugging.md) - SDK-wide debugging +- [MCP Specification](https://modelcontextprotocol.io/) - Official protocol docs diff --git a/docs/mcp.md b/docs/mcp/overview.md similarity index 96% rename from docs/mcp.md rename to docs/mcp/overview.md index b67dd7ca..ed8eaa5e 100644 --- a/docs/mcp.md +++ b/docs/mcp/overview.md @@ -255,20 +255,18 @@ directories for different applications. | "Timeout" errors | Increase the `timeout` value or check server performance | | Tools work but aren't called | Ensure your prompt clearly requires the tool's functionality | -### Debugging tips - -1. **Enable verbose logging** in your MCP server to see incoming requests -2. **Test your MCP server independently** before integrating with the SDK -3. **Start with a simple tool** to verify the integration works +For detailed debugging guidance, see the **[MCP Debugging Guide](./debugging.md)**. ## Related Resources - [Model Context Protocol Specification](https://modelcontextprotocol.io/) - [MCP Servers Directory](https://github.com/modelcontextprotocol/servers) - Community MCP servers - [GitHub MCP Server](https://github.com/github/github-mcp-server) - Official GitHub MCP server -- [Getting Started Guide](./getting-started.md) - SDK basics and custom tools +- [Getting Started Guide](../getting-started.md) - SDK basics and custom tools +- [General Debugging Guide](../debugging.md) - SDK-wide debugging ## See Also +- [MCP Debugging Guide](./debugging.md) - Detailed MCP troubleshooting - [Issue #9](https://github.com/github/copilot-sdk/issues/9) - Original MCP tools usage question - [Issue #36](https://github.com/github/copilot-sdk/issues/36) - MCP documentation tracking issue