Skill: alopez3006/snipara-mcp (version 0.1.0)

name: Snipara MCP - Smart Documentation Search description: Find answers in your codebase 10x faster with semantic search. Query multiple repos at once. AI remembers your preferences across sessions. --- # System Instructions for AI Agents You are an AI assistant with access to Snipara MCP tools. Use these tools to provide faster, more accurate answers by querying pre-indexed documentation instead of reading files one-by-one. ## When to Use Snipara Tools ✅ **USE Snipara when:** - User asks "How does X work?" → Use `rlm_ask` or `rlm_context_query` - User asks "Where do we do X?" → Use `rlm_multi_project_query` (if Team plan) - User asks "What are my preferences?" → Use `rlm_recall` - You need to find code patterns across multiple repos - You need to understand architecture/documentation ❌ **DON'T use Snipara when:** - User asks you to read a SPECIFIC file they named → Use native Read tool - User asks you to edit/write code → Use native Edit/Write tools - User asks to run commands → Use native Bash tool - Snipara API returns error → Fall back to native tools ## Recommended Workflow 1. **First query:** Always start with `rlm_ask` (fast, works on Free plan) 2. **If not found:** Try `rlm_context_query` with `search_mode: "semantic"` (requires Pro) 3. **Multi-repo search:** Use `rlm_multi_project_query` if user has Team plan 4. **Remember decisions:** Use `rlm_remember` to store important info for future sessions ## Error Handling If Snipara returns an error: - Check if user has required plan (Free/Pro/Team) - Check if docs are indexed (user needs to upload to dashboard first) - Fall back to native Read/Grep/Glob tools - Explain to user what went wrong and suggest solution ## Example Usage ``` User: "How does our authentication system work?" Good response: 1. Call rlm_ask("authentication system") 2. Get relevant docs in 2 seconds 3. Synthesize answer from returned context Bad response: 1. Use Grep to search for "auth" 2. Read 10 files sequentially 3. Hit token limits 4. Give incomplete answer ``` --- # Get Started in 2 Minutes ## The Problem You Have Right Now Your AI assistant searches files one-by-one using grep/find. With large codebases: - ❌ Queries take 20+ seconds - ❌ AI reads 50K tokens to answer simple questions - ❌ You manually search 5 repos to find "how we do X" - ❌ AI forgets your preferences next session ## The Solution (30 seconds from now) ```bash # 1. Install pip install snipara-mcp # Python npm install snipara-mcp # Node.js # 2. Get your API key # Sign up at https://snipara.com (Free: 100 queries/month) # 3. Set environment variable export SNIPARA_API_KEY="your-key-here" # 4. Add to your MCP client (Claude Code, Cline, Roo Code, etc.) # Done! Start using rlm_ask() in your next chat ``` ## Your First Query (Try This Now) ``` You: "How does authentication work in my codebase?" Behind the scenes: rlm_context_query("authentication") → 2 seconds later → Returns top 3 relevant docs (3K tokens instead of 50K) Result: Instant, accurate answer ``` **Note:** Before querying, index your docs at https://snipara.com/dashboard (upload .md/.txt/.mdx files). --- ## Core Capabilities (Pick What You Need) ### 🎯 Quick Answers (Start Here) **Plan Required:** ✅ FREE (100 queries/mo) **Tool:** `rlm_ask` **Use when:** You need a fast answer from your docs **Example:** `rlm_ask("API rate limits")` **Time saved:** 20 seconds → 2 seconds per query ```json { "query": "How do we handle webhooks?" } ``` --- ### 🔍 Deep Research (Complex Questions) **Plan Required:** ✅ FREE (keyword only) | 🔥 PRO ($19/mo for semantic) **Tool:** `rlm_context_query` **Use when:** You need semantic search with precise token control **Example:** Find conceptually related content, not just keyword matches **Benefit:** 90% context reduction (500K → 5K tokens) ```json { "query": "authentication implementation", "max_tokens": 6000, "search_mode": "hybrid" } ``` **Search modes by plan:** - `keyword` - Fast term matching ✅ **FREE** - `semantic` - Embedding similarity 🔥 **PRO+** - `hybrid` - Best of both worlds 🔥 **PRO+** --- ### 🌐 Multi-Repo Search **Plan Required:** 👥 TEAM ($49/mo) or ENTERPRISE **Tool:** `rlm_multi_project_query` **Use when:** You have 5+ repos and don't know which has the answer **Example:** One query searches ALL your team's projects **Time saved:** 5 minutes of manual searching → 3 seconds ```json { "query": "Where do we send email notifications?", "project_ids": [], "max_tokens": 8000 } ``` ⚠️ **Not available on Free/Pro plans** - Requires Team plan for multi-project access. --- ### 🧠 AI Memory (Remember Preferences) **Plan Required:** 🔥 PRO ($39/mo Agents) or 👥 TEAM ($79/mo Agents) **Tools:** `rlm_remember` + `rlm_recall` **Use when:** You want AI to remember your coding style/decisions **Benefit:** Consistent code across sessions **Store a memory:** ```json { "content": "User prefers TypeScript strict mode with functional components", "type": "preference", "scope": "project" } ``` **Recall later:** ```json { "query": "What are my coding preferences?", "limit": 5 } ``` **Memory types:** `fact`, `decision`, `learning`, `preference`, `todo`, `context` ⚠️ **Requires separate Agents plan** - Memory is part of Agents features, not Context plans. --- ### 👥 Team Standards (Auto-Enforce Rules) **Plan Required:** 👥 TEAM ($49/mo) or ENTERPRISE **Tool:** `rlm_shared_context` **Use when:** Your team needs consistent coding practices **Setup once:** Upload coding standards to Shared Collection **Every dev gets:** Auto-injected team rules in every query ```json { "categories": ["MANDATORY", "BEST_PRACTICES"], "max_tokens": 4000 } ``` **Categories by priority:** - `MANDATORY` - Non-negotiable rules (security, architecture) - `BEST_PRACTICES` - Recommended patterns (40% token budget) - `GUIDELINES` - Helpful suggestions - `REFERENCE` - Background info ⚠️ **Not available on Free/Pro plans** - Team-wide features require Team plan. --- ### 🔧 Power User Tools **Multi-Query (Parallel Searches):** ```json { "queries": [ { "query": "auth flow", "max_tokens": 3000 }, { "query": "session management", "max_tokens": 3000 } ] } ``` **Decompose (Break Down Complex Questions):** ```json { "query": "Explain the complete payment system architecture" } ``` **Plan (Preview Execution):** ```json { "query": "Find all API endpoints", "strategy": "relevance_first" } ``` **Search (Regex Pattern Matching):** ```json { "pattern": "async def|async function", "max_results": 20 } ``` **Session Context (Inject Standards):** ```json { "context": "Use Python 3.11+, prefer dataclasses over Pydantic" } ``` --- ### 📄 Document Management **Upload Single Doc:** ```json { "path": "docs/api.md", "content": "# API Documentation..." } ``` **Bulk Sync (CI/CD Integration):** ```json { "documents": [ { "path": "docs/auth.md", "content": "..." }, { "path": "docs/api.md", "content": "..." } ], "delete_missing": false } ``` **Check Stats:** ```json {} ``` --- ## ROI Calculator ### Scenario 1: Solo Developer (Large Codebase) **Current pain:** Grep/find searches take 20+ seconds, read 50K tokens per query | Metric | Before Snipara | With Snipara | Savings | | -------------- | -------------- | ------------ | ------------------------ | | Query speed | 20 seconds | 2 seconds | 18 seconds | | Daily queries | 50 | 50 | - | | Time per day | 16 minutes | 1.6 minutes | **14.4 min/day** | | Time per month | 7.2 hours | 0.72 hours | **6.5 hours/month** | | **Cost** | **$0** | **$0-19/mo** | **ROI: 6.5 hours saved** | **Plan recommendation:** Start with **FREE** (100 queries), upgrade to **PRO** ($19/mo) if you need semantic search. --- ### Scenario 2: Team (5+ Repositories) **Current pain:** Switch between 5 projects manually, 5 minutes per search | Metric | Before Snipara | With Snipara | Savings | | ----------------- | -------------- | --------------- | ------------------------- | | Multi-repo search | 5 min | 3 seconds | 4.97 min | | Searches per day | 10 | 10 | - | | Time per day | 50 minutes | 30 seconds | **49.5 min/day** | | Time per month | 24.75 hours | 0.25 hours | **24.5 hours/month** | | **Cost** | **$0** | **$49/mo Team** | **ROI: 24.5 hours saved** | **Plan recommendation:** **TEAM** ($49/mo) for `rlm_multi_project_query` + shared standards. --- ### Scenario 3: Enterprise (Consistent Standards) **Current pain:** 10 devs ask "how do we do X?" daily, inconsistent code | Before | With Snipara Shared Context | | ------------------------------ | ----------------------------------------- | | ❌ Each dev googles/asks Slack | ✅ Standards auto-injected in every query | | ❌ Inconsistent patterns | ✅ Enforced team conventions | | ❌ Onboarding takes 2 weeks | ✅ New devs get standards instantly | | ❌ Code review conflicts | ✅ Code follows standards from day 1 | **Cost:** $49/mo Team or $499/mo Enterprise **ROI:** Consistency + faster onboarding = easily 20+ hours/month saved --- ## Quick Start by Use Case ### Use Case 1: "I have huge docs and grep is slow" **Plan:** ✅ FREE (100 queries/mo) ```bash # 1. Index your docs once Visit https://snipara.com/dashboard → Create project → Upload .md/.txt files # 2. Query instantly rlm_ask("How does authentication work?") ``` --- ### Use Case 2: "I work on 10 microservices" **Plan:** 👥 TEAM ($49/mo) ```bash # 1. Create 10 projects on Snipara dashboard # 2. Enable Team plan # 3. Query all repos at once rlm_multi_project_query("How do we handle rate limiting?") ``` ⚠️ **Requires Team plan** - Multi-project search not available on Free/Pro. --- ### Use Case 3: "AI keeps forgetting my preferences" **Plan:** 🔥 PRO Agents ($39/mo) or 👥 TEAM Agents ($79/mo) ```bash # 1. Enable Agents plan (separate from Context plan) # 2. Store your preferences once rlm_remember(type="preference", content="Use functional React components") # 3. AI recalls them forever rlm_recall("my coding preferences") ``` ⚠️ **Requires separate Agents subscription** - Memory features not included in Context plans. --- ## Pricing (Two Subscription Types) ### Context Plans (Documentation Search) | Plan | Price | Queries/mo | Search Mode | Multi-Project | | -------------- | ------- | ---------- | ----------------- | ------------- | | **FREE** | $0 | 100 | Keyword only | ❌ | | **PRO** | $19/mo | 5,000 | Semantic + Hybrid | ❌ | | **TEAM** | $49/mo | 20,000 | Semantic + Hybrid | ✅ | | **ENTERPRISE** | $499/mo | Unlimited | Semantic + Hybrid | ✅ | ### Agents Plans (Memory & Swarms) | Plan | Price | Prerequisite | Features | | -------------- | ------- | ------------------ | --------------------------- | | **STARTER** | $15/mo | None | Basic memory (100 memories) | | **PRO** | $39/mo | None | Unlimited memories, swarms | | **TEAM** | $79/mo | Context TEAM+ | Team-wide memory sharing | | **ENTERPRISE** | $199/mo | Context ENTERPRISE | Advanced coordination | ⚠️ **Two separate subscriptions:** Context plans for search, Agents plans for memory/swarms. **Try free first:** 100 queries is ~5 days of usage to test value. --- ## Example Workflows ### Example 1: Quick Answer (FREE plan) ``` User: "What are our API rate limits?" You call: rlm_ask("API rate limits") Result: Returns relevant docs in 2 seconds ``` --- ### Example 2: Semantic Search (PRO plan) ``` User: "How do we validate user input?" You call: rlm_context_query("user input validation", search_mode="semantic") Result: Finds docs about "sanitization", "XSS prevention", "schema validation" even if they don't contain exact keywords ``` --- ### Example 3: Multi-Repo Search (TEAM plan) ``` User: "Show me all webhook implementations across our projects" You call: rlm_multi_project_query("webhook implementation") Result: Returns implementations from all 10 microservices in 3 seconds ``` --- ### Example 4: Persistent Memory (PRO Agents plan) ``` Session 1 (Monday): User: "I prefer TypeScript strict mode and functional components" You call: rlm_remember(type="preference", content="Prefers TS strict + functional") Session 2 (Friday - NEW SESSION): User: "Create a new React component" You call: rlm_recall("coding preferences") Result: AI remembers to use functional components from Monday! ``` --- ### Example 5: Team Standards (TEAM plan) ``` Setup (Admin does once): - Upload coding standards to Shared Context Collection - Link collection to all team projects Every developer: User: "Write a new API endpoint" You call: rlm_shared_context(categories=["MANDATORY"]) Result: Auto-injects team's API design rules, security requirements, etc. ``` --- ## Support & Resources - **Website:** https://snipara.com - **Documentation:** https://docs.snipara.com - **GitHub:** https://github.com/snipara/snipara-mcp - **Issues:** https://github.com/snipara/snipara-mcp/issues - **Email:** [REDACTED_SECRET] --- ## Quick Tips 1. **Start small:** Use `rlm_ask` for quick answers on FREE plan 2. **Upgrade smart:** Get PRO when keyword search isn't finding what you need 3. **Team value:** Multi-project search pays for itself with 5+ repos 4. **Memory requires separate plan:** Context + Agents are two subscriptions 5. **Index first:** Upload docs to dashboard before querying **When in doubt, start with FREE and upgrade based on value received. 🚀** --- ## Complete Tool Reference (For Power Users) ### Query Tools (All Plans) **rlm_ask** - Quick keyword search ```json { "query": "API rate limits" } ``` **rlm_context_query** - Full-featured semantic search ```json { "query": "authentication", "max_tokens": 6000, "search_mode": "hybrid", "include_metadata": true } ``` **rlm_search** - Regex pattern search ```json { "pattern": "async def|async function", "max_results": 20 } ``` **rlm_inject** - Set session context ```json { "context": "Use Python 3.11+, prefer dataclasses", "append": false } ``` **rlm_context** - Show current session context ```json {} ``` **rlm_clear_context** - Clear session context ```json {} ``` --- ### Advanced Query Tools (Pro+) **rlm_multi_query** - Parallel queries ```json { "queries": [ { "query": "auth flow", "max_tokens": 3000 }, { "query": "session management", "max_tokens": 3000 } ], "max_tokens": 8000 } ``` **rlm_decompose** - Break down complex questions ```json { "query": "Explain payment system architecture", "max_depth": 2 } ``` **rlm_plan** - Generate execution plan ```json { "query": "Find all API endpoints", "strategy": "relevance_first", "max_tokens": 16000 } ``` --- ### Team Tools (Team+ Plan) **rlm_multi_project_query** - Search across all repos ```json { "query": "webhook implementation", "project_ids": [], "exclude_project_ids": [], "max_tokens": 8000, "per_project_limit": 3 } ``` **rlm_shared_context** - Get team standards ```json { "categories": ["MANDATORY", "BEST_PRACTICES"], "max_tokens": 4000, "include_content": true } ``` **rlm_list_templates** - Browse prompt templates ```json { "category": "code-review" } ``` **rlm_get_template** - Use template with variables ```json { "slug": "security-review", "variables": { "author": "John", "pr_number": "123" } } ``` **rlm_list_collections** - List shared collections ```json { "include_public": true } ``` **rlm_upload_shared_document** - Upload to shared collection ```json { "collection_id": "col_abc123", "title": "TypeScript Standards", "content": "# Standards...", "category": "BEST_PRACTICES", "priority": 90 } ``` --- ### Memory Tools (Agents Plan) **rlm_remember** - Store memory ```json { "content": "User prefers functional components", "type": "preference", "scope": "project", "category": "coding-style", "ttl_days": null } ``` **rlm_recall** - Query memories ```json { "query": "What are my preferences?", "type": "preference", "limit": 5, "min_relevance": 0.5 } ``` **rlm_memories** - List all memories ```json { "type": "preference", "category": "coding-style", "limit": 20, "offset": 0 } ``` **rlm_forget** - Delete memories ```json { "memory_id": "mem_abc123" } ``` --- ### Document Management Tools **rlm_upload_document** - Upload single doc ```json { "path": "docs/api.md", "content": "# API Documentation..." } ``` **rlm_sync_documents** - Bulk upload ```json { "documents": [ { "path": "docs/auth.md", "content": "..." }, { "path": "docs/api.md", "content": "..." } ], "delete_missing": false } ``` **rlm_store_summary** - Store document summary ```json { "document_path": "docs/api.md", "summary": "RESTful API with OAuth2 auth...", "summary_type": "concise", "generated_by": "claude-3.5-sonnet" } ``` **rlm_get_summaries** - Get stored summaries ```json { "document_path": "docs/api.md", "summary_type": "concise" } ``` **rlm_stats** - Get documentation stats ```json {} ``` **rlm_sections** - List indexed sections ```json { "filter": "auth", "limit": 50, "offset": 0 } ``` **rlm_read** - Read specific lines ```json { "start_line": 1, "end_line": 100 } ``` --- ### Advanced Features (Enterprise) **rlm_swarm_create** - Create agent swarm ```json { "name": "code-review-swarm", "description": "Parallel code review", "max_agents": 10 } ``` **rlm_swarm_join** - Join swarm ```json { "swarm_id": "swarm_abc123", "agent_id": "agent_1", "role": "worker", "capabilities": ["review", "test"] } ``` **rlm_claim** - Claim resource for exclusive access ```json { "swarm_id": "swarm_abc123", "agent_id": "agent_1", "resource_type": "file", "resource_id": "src/auth.ts", "timeout_seconds": 300 } ``` **rlm_release** - Release claimed resource ```json { "swarm_id": "swarm_abc123", "agent_id": "agent_1", "claim_id": "claim_abc123" } ``` **rlm_state_get** - Read swarm state ```json { "swarm_id": "swarm_abc123", "key": "progress" } ``` **rlm_state_set** - Write swarm state ```json { "swarm_id": "swarm_abc123", "agent_id": "agent_1", "key": "progress", "value": { "completed": 5, "total": 10 }, "expected_version": 1 } ``` **rlm_broadcast** - Broadcast event to swarm ```json { "swarm_id": "swarm_abc123", "agent_id": "agent_1", "event_type": "task_completed", "payload": { "task_id": "task_1" } } ``` **rlm_task_create** - Create swarm task ```json { "swarm_id": "swarm_abc123", "agent_id": "agent_1", "title": "Review auth module", "description": "Security review", "priority": 90 } ``` **rlm_task_claim** - Claim task from queue ```json { "swarm_id": "swarm_abc123", "agent_id": "agent_1", "task_id": "task_abc123" } ``` **rlm_task_complete** - Mark task complete ```json { "swarm_id": "swarm_abc123", "agent_id": "agent_1", "task_id": "task_abc123", "success": true, "result": { "issues_found": 0 } } ``` --- ## Settings & Configuration **rlm_settings** - Get project settings ```json { "refresh": false } ``` Returns current project configuration including: - Max tokens per query - Default search mode - Rate limits - Enabled features --- **For complete API documentation, visit: https://docs.snipara.com**

path: README.md
content: # Snipara MCP

[![PyPI version](https://badge.fury.io/py/snipara-mcp.svg)](https://pypi.org/project/snipara-mcp/)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

**Model Context Protocol (MCP) server for [Snipara](https://snipara.com) - Context optimization and Agent infrastructure for LLMs.**

![Snipara MCP Demo](https://snipara.com/images/mcp-demo.gif)

## Overview

Snipara MCP is a client package that connects AI assistants to the [Snipara API](https://snipara.com), enabling:

- **🔍 90% Context Reduction** - Query 500K tokens, get 5K relevant results
- **🌐 Multi-Project Search** - Search across all your repos in one query
- **🧠 AI Memory** - Persistent semantic memory across sessions
- **👥 Team Standards** - Auto-inject coding standards into every query
- **🤖 Multi-Agent Swarms** - Coordinate multiple AI agents with shared state

### How It Works

```
┌─────────────────────────────────────────────────────────────────┐
│  Your AI Assistant (Claude, GPT, Cursor, etc.)                 │
│  ├── Asks: "How does authentication work?"                     │
│  └── Uses: rlm_context_query("authentication")                 │
└────────────────────────────┬────────────────────────────────────┘
                             │
                   ┌─────────▼─────────┐
                   │  Snipara MCP      │  (This package)
                   │  Client Library   │
                   └─────────┬─────────┘
                             │ HTTPS
                             ▼
                   ┌─────────────────────┐
                   │  Snipara API        │
                   │  api.snipara.com    │
                   │  - Indexed docs     │
                   │  - Semantic search  │
                   │  - Agent memory     │
                   └─────────┬───────────┘
                             │
                   ┌─────────▼─────────┐
                   │  Returns: 3K      │
                   │  relevant tokens  │
                   │  instead of 50K   │
                   └───────────────────┘
```

**Key Insight:** This is the **client package** - it connects to Snipara's API to fetch optimized context. The server-side implementation is private.

## What's In This Repository

This is the **open-source MCP client package** published to PyPI as [`snipara-mcp`](https://pypi.org/project/snipara-mcp/).

```
snipara-mcp/
├── src/snipara_mcp/
│   ├── __init__.py         # Package entry point
│   ├── server.py           # MCP stdio transport (2,050 lines)
│   ├── rlm_tools.py        # 39 tool implementations (2,117 lines)
│   └── auth.py             # OAuth device flow (402 lines)
├── skill.md                # ClawdHub marketplace documentation
├── pyproject.toml          # PyPI package configuration
└── README.md               # This file
```

**What's NOT in this repo (private):**
- FastAPI backend server
- Database models and search algorithms
- Rate limiting and billing logic
- Internal API endpoints

## Quick Start

### Installation

**Option 1: uvx (No Install Required)**
```bash
uvx snipara-mcp
```

**Option 2: pip**
```bash
pip install snipara-mcp
```

**Option 3: With RLM Runtime**
```bash
pip install snipara-mcp[rlm]
```

### Configuration

Get your API key from [snipara.com/dashboard](https://snipara.com/dashboard)

#### Claude Desktop

Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "snipara": {
      "command": "uvx",
      "args": ["snipara-mcp"],
      "env": {
        "SNIPARA_API_KEY": "rlm_your_key",
        "SNIPARA_PROJECT_ID": "your_project_id"
      }
    }
  }
}
```

#### Cursor

Add to `~/.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "snipara": {
      "command": "uvx",
      "args": ["snipara-mcp"],
      "env": {
        "SNIPARA_API_KEY": "rlm_your_key",
        "SNIPARA_PROJECT_ID": "your_project_id"
      }
    }
  }
}
```

#### Claude Code

```bash
claude mcp add snipara -- uvx snipara-mcp
export SNIPARA_API_KEY="rlm_your_key"
export SNIPARA_PROJECT_ID="your_project_id"
```

#### Windsurf

Add to `~/.codeium/windsurf/mcp_config.json`:

```json
{
  "mcpServers": {
    "snipara": {
      "command": "uvx",
      "args": ["snipara-mcp"],
      "env": {
        "SNIPARA_API_KEY": "rlm_your_key",
        "SNIPARA_PROJECT_ID": "your_project_id"
      }
    }
  }
}
```

### Your First Query

Once configured, ask your AI:

> "Use snipara to find how authentication works"

Behind the scenes:
```
rlm_context_query("authentication")
→ 2 seconds later
→ Returns top 3 relevant docs (3K tokens instead of 50K)
```

## Features

### 🎯 Context Optimization (All Plans)

**Tools:** `rlm_context_query`, `rlm_ask`, `rlm_search`

Query your documentation with semantic search and get only relevant results within your token budget.

```json
{
  "query": "authentication implementation",
  "max_tokens": 6000,
  "search_mode": "hybrid"
}
```

### 🌐 Multi-Project Search (Team+)

**Tool:** `rlm_multi_project_query`

Search across all your team's repositories in one query.

```json
{
  "query": "How do we handle webhooks?",
  "project_ids": [],
  "max_tokens": 8000
}
```

### 🧠 AI Memory (Agents Plans)

**Tools:** `rlm_remember`, `rlm_recall`

Persistent semantic memory across sessions with confidence decay.

```json
// Store once
{
  "content": "User prefers TypeScript strict mode",
  "type": "preference",
  "scope": "project"
}

// Recall later
{
  "query": "What are my coding preferences?",
  "limit": 5
}
```

### 👥 Team Standards (Team+)

**Tool:** `rlm_shared_context`

Auto-inject team coding standards into every query.

```json
{
  "categories": ["MANDATORY", "BEST_PRACTICES"],
  "max_tokens": 4000
}
```

### 🤖 Multi-Agent Swarms (Enterprise)

**Tools:** `rlm_swarm_create`, `rlm_claim`, `rlm_task_create`, etc.

Coordinate multiple AI agents with:
- Shared state management
- Resource claims (prevent conflicts)
- Distributed task queue
- Event broadcasting

```json
// Create swarm
{ "name": "code-review-swarm", "max_agents": 5 }

// Claim file access
{ "swarm_id": "...", "resource_type": "file", "resource_id": "src/auth.ts" }

// Create task
{ "swarm_id": "...", "title": "Review auth module", "priority": 90 }
```

## Complete Tool Reference

### Primary Tools
- **rlm_context_query** - Semantic search with token budget
- **rlm_ask** - Quick keyword search
- **rlm_search** - Regex pattern search

### Advanced Tools
- **rlm_multi_query** - Parallel queries
- **rlm_decompose** - Break down complex questions
- **rlm_plan** - Generate execution plan
- **rlm_inject** - Set session context

### Team Tools
- **rlm_multi_project_query** - Cross-repo search
- **rlm_shared_context** - Team standards
- **rlm_list_templates** - Prompt templates
- **rlm_upload_shared_document** - Upload team docs

### Memory Tools (Agents Plan)
- **rlm_remember** - Store memory
- **rlm_recall** - Semantic recall
- **rlm_memories** - List all memories
- **rlm_forget** - Delete memories

### Swarm Tools (Enterprise)
- **rlm_swarm_create** / **rlm_swarm_join**
- **rlm_claim** / **rlm_release**
- **rlm_state_get** / **rlm_state_set**
- **rlm_broadcast**
- **rlm_task_create** / **rlm_task_claim** / **rlm_task_complete**

### Document Management
- **rlm_upload_document** - Upload single doc
- **rlm_sync_documents** - Bulk sync
- **rlm_store_summary** - Store LLM-generated summary
- **rlm_stats** - Documentation statistics

**Total:** 39 tools

Full documentation: [docs.snipara.com](https://docs.snipara.com)

## Pricing

### Context Plans (Documentation Search)

| Plan       | Price   | Queries/mo | Search Mode       | Multi-Project |
| ---------- | ------- | ---------- | ----------------- | ------------- |
| FREE       | $0      | 100        | Keyword only      | ❌            |
| PRO        | $19/mo  | 5,000      | Semantic + Hybrid | ❌            |
| TEAM       | $49/mo  | 20,000     | Semantic + Hybrid | ✅            |
| ENTERPRISE | $499/mo | Unlimited  | Semantic + Hybrid | ✅            |

### Agents Plans (Memory & Swarms)

| Plan       | Price   | Features                                    |
| ---------- | ------- | ------------------------------------------- |
| STARTER    | $15/mo  | Basic memory (100 memories)                 |
| PRO        | $39/mo  | Unlimited memories, swarms                  |
| TEAM       | $79/mo  | Team-wide memory (requires Context TEAM+)   |
| ENTERPRISE | $199/mo | Advanced coordination (requires Context ENT |

**Note:** Context and Agents are separate subscriptions.

## Development

### Prerequisites

- Python 3.10+
- pip or uv

### Local Setup

```bash
# Clone the repo
git clone https://github.com/alopez3006/snipara-mcp.git
cd snipara-mcp

# Install dependencies
pip install -e .

# Set environment variables
export SNIPARA_API_KEY="rlm_your_key"
export SNIPARA_PROJECT_ID="your_project_id"

# Test the package
python -m snipara_mcp
```

### Project Structure

```
src/snipara_mcp/
├── __init__.py         # Package entry point, CLI main()
├── server.py           # MCP stdio transport implementation
│                       # - Handles MCP protocol (JSON-RPC)
│                       # - Tool registration and dispatch
│                       # - Prompt auto-injection
│
├── rlm_tools.py        # 39 tool implementations
│                       # - Context optimization tools
│                       # - Agent memory tools
│                       # - Multi-agent swarm tools
│                       # - Document management
│
└── auth.py             # OAuth device flow authentication
                        # - Login/logout/status CLI commands
                        # - Token storage and refresh
```

### Testing

```bash
# Test basic connection
uvx snipara-mcp

# Test with specific tool
echo '{"query": "test"}' | uvx snipara-mcp

# Test OAuth flow
snipara-mcp-login
snipara-mcp-status
```

### Publishing to PyPI

```bash
# Bump version in pyproject.toml
# version = "2.2.1"

# Build distribution
python -m build

# Upload to PyPI
python -m twine upload dist/*
```

## Architecture

### MCP Protocol

This package implements the [Model Context Protocol](https://modelcontextprotocol.io/) stdio transport:

```
AI Client → stdin → snipara-mcp → HTTPS → api.snipara.com
AI Client ← stdout ← snipara-mcp ← JSON ← api.snipara.com
```

### Authentication

Supports two authentication methods:

1. **API Key** (Recommended)
   ```bash
   export SNIPARA_API_KEY="rlm_..."
   ```

2. **OAuth Device Flow**
   ```bash
   snipara-mcp-login
   # Follow browser prompt to authorize
   ```

Tokens are stored in `~/.snipara/credentials.json`

### Rate Limiting

Rate limits are enforced server-side based on your plan:
- FREE: 100 queries/month
- PRO: 5,000 queries/month
- TEAM: 20,000 queries/month
- ENTERPRISE: Unlimited

## Upgrading

When a new version is released:

### 1. Clear uvx cache
```bash
# macOS/Linux
rm -rf ~/.cache/uv/tools/snipara-mcp
rm -rf ~/Library/Caches/uv/tools/snipara-mcp

# Windows
rmdir /s %LOCALAPPDATA%\uv\tools\snipara-mcp
```

### 2. Restart your MCP client

MCP tool definitions are loaded at startup. Restart Claude Desktop, Cursor, or your MCP client.

### 3. Verify version

```bash
snipara-mcp-status
```

## Troubleshooting

### MCP tools not showing up

1. **Restart your MCP client** - Tools are cached at startup
2. **Clear uvx cache** - See Upgrading section
3. **Check config syntax** - Ensure valid JSON in MCP config

### "Invalid API key" error

- Verify API key in [dashboard](https://snipara.com/dashboard)
- Check for whitespace in config
- Try OAuth: `snipara-mcp-login`

### MCP server not connecting

- Check `uvx` is installed: `uvx --version`
- Install uv: `curl -LsSf https://astral.sh/uv/install.sh | sh`
- Check Claude Code output panel for errors

## RLM Runtime Integration

Use Snipara tools programmatically in Python:

```python
from rlm import RLM

# Snipara tools auto-registered when credentials set
rlm = RLM(
    model="claude-sonnet-4-20250514",
    snipara_api_key="rlm_your_key",
    snipara_project_slug="your-project"
)

# LLM can now query your docs during execution
result = rlm.run("Implement auth following our coding standards")
```

See: [rlm-runtime documentation](https://github.com/alopez3006/rlm-runtime)

## CI/CD Integration

Auto-sync docs on git push:

```bash
curl -X POST "https://api.snipara.com/v1/YOUR_PROJECT/webhook/sync" \
  -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{"documents": [{"path": "docs/api.md", "content": "..."}]}'
```

See: [GitHub Action example](#) (coming soon)

## Support

- **Website:** [snipara.com](https://snipara.com)
- **Documentation:** [docs.snipara.com](https://docs.snipara.com)
- **Issues:** [GitHub Issues](https://github.com/alopez3006/snipara-mcp/issues)
- **Email:** [REDACTED_SECRET]
- **Discord:** [Join our community](https://discord.gg/snipara)

## Contributing

We welcome contributions! Please:

1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests if applicable
5. Submit a pull request

**Development Guidelines:**
- Follow existing code style
- Add docstrings to public functions
- Update README if adding features
- Test with multiple MCP clients

## Version History

| Version | Date       | Changes                                             |
| ------- | ---------- | --------------------------------------------------- |
| 2.2.0   | 2026-01-29 | Separate public repository, update repo URL         |
| 2.1.0   | 2025-01-25 | Full tool parity with FastAPI (39 tools)            |
| 1.8.1   | 2025-01-25 | Add multi_project_query for cross-repo search       |
| 1.7.6   | 2025-01-24 | Redis URL protocol support                          |
| 1.7.0   | 2025-01-21 | OAuth device flow authentication                    |
| 1.6.0   | 2025-01-20 | Agent Memory and Multi-Agent Swarms (14 new tools)  |
| 1.5.0   | 2025-01-18 | Auto-inject Snipara usage instructions              |
| 1.4.0   | 2025-01-15 | RLM Runtime integration                             |
| 1.3.0   | 2025-01-10 | Shared Context tools (Team+)                        |
| 1.2.0   | 2025-01-05 | Document upload and sync tools                      |
| 1.0.0   | 2024-12-15 | Initial release with core context optimization      |

## License

MIT License - see [LICENSE](LICENSE) file

---

**Built with ❤️ by the Snipara team**

[Website](https://snipara.com) • [Documentation](https://docs.snipara.com) • [PyPI](https://pypi.org/project/snipara-mcp/) • [GitHub](https://github.com/alopez3006/snipara-mcp)


path: pyproject.toml
content: [project]
name = "snipara-mcp"
version = "2.2.0"
description = "MCP server for Snipara - Context optimization and Agent infrastructure for LLMs"
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT" }
authors = [{ name = "Snipara", email = "[REDACTED_SECRET]" }]
keywords = ["mcp", "llm", "context", "documentation", "snipara", "claude", "cursor", "rlm", "agents", "multi-agent", "swarm", "memory"]
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
]

dependencies = [
    "mcp>=1.25.0",
    "httpx>=0.26.0",
]

[project.optional-dependencies]
rlm = ["rlm-runtime>=0.1.0"]

[project.scripts]
snipara-mcp = "snipara_mcp:main"
snipara-mcp-login = "snipara_mcp.auth:login_cli"
snipara-mcp-logout = "snipara_mcp.auth:logout_cli"
snipara-mcp-status = "snipara_mcp.auth:status_cli"

[project.urls]
Homepage = "https://snipara.com"
Documentation = "https://docs.snipara.com"
Repository = "https://github.com/alopez3006/snipara-mcp"

[build-system]
requires = ["hatchling<1.26"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/snipara_mcp"]

[tool.hatch.metadata]
license-files = []

[tool.ruff]
line-length = 100
target-version = "py310"


path: src/snipara_mcp/__init__.py
content: """Snipara MCP Server - Context optimization for LLMs."""

from .server import main

__version__ = "2.1.0"
__all__ = ["main", "get_snipara_tools"]


def get_snipara_tools(*args, **kwargs):
    """Get Snipara tools for rlm-runtime integration.

    This is a lazy import to avoid requiring rlm-runtime
    when using snipara-mcp as a standalone MCP server.

    See rlm_tools.get_snipara_tools for full documentation.
    """
    from .rlm_tools import get_snipara_tools as _get_snipara_tools
    return _get_snipara_tools(*args, **kwargs)


path: src/snipara_mcp/auth.py
content: #!/usr/bin/env python3
"""
OAuth Device Flow authentication for Snipara MCP.

This module implements the OAuth 2.0 Device Authorization Flow (RFC 8628)
to allow users to authenticate without manually copying API keys.

Usage:
    snipara-mcp-login          # Interactive login
    snipara-mcp-logout         # Clear stored tokens
    snipara-mcp-status         # Show current auth status
"""

import asyncio
import json
import os
import stat
import sys
import webbrowser
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any

import httpx

# Token storage location
TOKEN_DIR = Path.home() / ".snipara"
TOKEN_FILE = TOKEN_DIR / "tokens.json"

# API configuration
DEFAULT_API_URL = "https://www.snipara.com"


def get_api_url() -> str:
    """Get the API URL from environment or default."""
    return os.environ.get("SNIPARA_API_URL", DEFAULT_API_URL)


def ensure_token_dir() -> None:
    """Ensure token directory exists with secure permissions."""
    if not TOKEN_DIR.exists():
        TOKEN_DIR.mkdir(parents=True, mode=0o700)


def load_tokens() -> dict[str, Any]:
    """Load stored tokens from file."""
    if not TOKEN_FILE.exists():
        return {}
    try:
        with open(TOKEN_FILE) as f:
            return json.load(f)
    except (json.JSONDecodeError, IOError):
        return {}


def save_tokens(tokens: dict[str, Any]) -> None:
    """Save tokens to file with secure permissions."""
    ensure_token_dir()
    with open(TOKEN_FILE, "w") as f:
        json.dump(tokens, f, indent=2)
    # Secure file permissions (owner read/write only)
    TOKEN_FILE.chmod(stat.S_IRUSR | stat.S_IWUSR)


def get_stored_token(project_id: str) -> dict[str, Any] | None:
    """
    Get stored token for a project.

    Args:
        project_id: The project ID to get token for

    Returns:
        Token data if valid, None otherwise
    """
    tokens = load_tokens()
    token_data = tokens.get(project_id)

    if not token_data:
        return None

    # Check if access token is expired
    expires_at = token_data.get("expires_at")
    if expires_at:
        try:
            exp_time = datetime.fromisoformat(expires_at)
            if exp_time < datetime.utcnow():
                # Token expired, try to refresh
                refresh_token = token_data.get("refresh_token")
                if refresh_token:
                    refreshed = asyncio.run(refresh_access_token(refresh_token))
                    if refreshed:
                        # Update stored token
                        tokens[project_id] = refreshed
                        save_tokens(tokens)
                        return refreshed
                # Refresh failed or no refresh token
                return None
        except (ValueError, TypeError):
            pass

    return token_data


def store_token(project_id: str, token_data: dict[str, Any]) -> None:
    """
    Store token for a project.

    Args:
        project_id: The project ID
        token_data: Token response from OAuth server
    """
    tokens = load_tokens()

    # Calculate expiration time
    expires_in = token_data.get("expires_in", 3600)
    expires_at = datetime.utcnow() + timedelta(seconds=expires_in)

    tokens[project_id] = {
        "access_token": token_data["access_token"],
        "refresh_token": token_data.get("refresh_token"),
        "expires_at": expires_at.isoformat(),
        "scope": token_data.get("scope"),
        "project_slug": token_data.get("project_slug"),
        "project_id": project_id,
    }

    save_tokens(tokens)


def remove_token(project_id: str) -> bool:
    """
    Remove stored token for a project.

    Args:
        project_id: The project ID

    Returns:
        True if token was removed, False if not found
    """
    tokens = load_tokens()
    if project_id in tokens:
        del tokens[project_id]
        save_tokens(tokens)
        return True
    return False


def clear_all_tokens() -> int:
    """
    Clear all stored tokens.

    Returns:
        Number of tokens cleared
    """
    tokens = load_tokens()
    count = len(tokens)
    if TOKEN_FILE.exists():
        TOKEN_FILE.unlink()
    return count


def get_auth_header(project_id: str) -> str | None:
    """
    Get authorization header for a project.

    Checks for OAuth token first, falls back to API key from environment.

    Args:
        project_id: The project ID

    Returns:
        Authorization header value or None
    """
    # First check for stored OAuth token
    token_data = get_stored_token(project_id)
    if token_data and token_data.get("access_token"):
        return f"Bearer {token_data['access_token']}"

    # Fall back to API key from environment
    [REDACTED_SECRET]"SNIPARA_API_KEY")
    if [REDACTED_SECRET] api_key  # Will be used as X-API-Key

    return None


async def refresh_access_token(refresh_token: str) -> dict[str, Any] | None:
    """
    Refresh an access token using a refresh token.

    Args:
        refresh_token: The refresh token

    Returns:
        New token data if successful, None otherwise
    """
    api_url = get_api_url()

    try:
        async with httpx.AsyncClient(timeout=30.0) as client:
            response = await client.post(
                f"{api_url}/api/oauth/token",
                json={
                    "grant_type": "refresh_token",
                    "refresh_token": refresh_token,
                },
            )

            if response.status_code == 200:
                data = response.json()
                expires_in = data.get("expires_in", 3600)
                expires_at = datetime.utcnow() + timedelta(seconds=expires_in)

                return {
                    "access_token": data["access_token"],
                    "refresh_token": data.get("refresh_token"),
                    "expires_at": expires_at.isoformat(),
                    "scope": data.get("scope"),
                    "project_slug": data.get("project_slug"),
                    "project_id": data.get("project_id"),
                }
    except Exception:
        pass

    return None


async def device_flow_login() -> dict[str, Any]:
    """
    Perform OAuth 2.0 Device Authorization Flow.

    Returns:
        Token data from successful authentication

    Raises:
        Exception: If authentication fails
    """
    api_url = get_api_url()

    async with httpx.AsyncClient(timeout=30.0) as client:
        # Step 1: Request device code
        print("\nRequesting device code...")
        response = await client.post(
            f"{api_url}/api/oauth/device/code",
            json={"client_id": "snipara-mcp"},
        )
        response.raise_for_status()
        device_data = response.json()

        user_code = device_data["user_code"]
        device_code = device_data["device_code"]
        verification_uri = device_data["verification_uri"]
        verification_uri_complete = device_data.get("verification_uri_complete")
        interval = device_data.get("interval", 5)
        expires_in = device_data.get("expires_in", 1800)

        # Step 2: Display instructions
        print("\n" + "=" * 50)
        print("  Snipara MCP Authentication")
        print("=" * 50)
        print()
        print("1. Open this URL in your browser:")
        print(f"   {verification_uri_complete or verification_uri}")
        print()
        print(f"2. Enter this code: {user_code}")
        print()
        print("3. Select your project and click 'Authorize'")
        print()
        print(f"Waiting for authorization... (expires in {expires_in // 60} minutes)")
        print("-" * 50)

        # Try to open browser automatically
        try:
            webbrowser.open(verification_uri_complete or verification_uri)
        except Exception:
            pass

        # Step 3: Poll for token
        start_time = datetime.utcnow()
        max_wait = timedelta(seconds=expires_in)

        while datetime.utcnow() - start_time < max_wait:
            await asyncio.sleep(interval)

            try:
                token_response = await client.post(
                    f"{api_url}/api/oauth/device/token",
                    json={
                        "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
                        "device_code": device_code,
                        "client_id": "snipara-mcp",
                    },
                )

                if token_response.status_code == 200:
                    token_data = token_response.json()
                    project_id = token_data.get("project_id")
                    project_slug = token_data.get("project_slug", project_id)

                    # Store token
                    store_token(project_id, token_data)

                    print()
                    print("=" * 50)
                    print("  Authentication Successful!")
                    print("=" * 50)
                    print()
                    print(f"  Project: {project_slug}")
                    print(f"  Token stored in: {TOKEN_FILE}")
                    print()
                    print("You can now use snipara-mcp with this project.")
                    print()

                    return token_data

                error_data = token_response.json()
                error = error_data.get("error")

                if error == "authorization_pending":
                    # Still waiting, continue polling
                    continue
                elif error == "slow_down":
                    # Increase interval
                    interval += 5
                    continue
                elif error == "access_denied":
                    raise Exception("Authorization denied by user")
                elif error == "expired_token":
                    raise Exception("Device code expired. Please try again.")
                else:
                    raise Exception(f"OAuth error: {error}")

            except httpx.HTTPStatusError as e:
                if e.response.status_code == 400:
                    # Might be authorization_pending, continue polling
                    continue
                raise

        raise Exception("Authorization timed out. Please try again.")


def login_cli() -> None:
    """CLI entry point for snipara-mcp-login."""
    print()
    print("Snipara MCP - Device Flow Login")
    print()

    try:
        asyncio.run(device_flow_login())
    except KeyboardInterrupt:
        print("\n\nLogin cancelled.")
        sys.exit(1)
    except Exception as e:
        print(f"\nError: {e}")
        sys.exit(1)


def logout_cli() -> None:
    """CLI entry point for snipara-mcp-logout."""
    print()
    print("Snipara MCP - Logout")
    print()

    tokens = load_tokens()
    if not tokens:
        print("No stored tokens found.")
        return

    print(f"Found {len(tokens)} stored token(s):")
    for project_id, data in tokens.items():
        slug = data.get("project_slug", project_id)
        print(f"  - {slug}")

    print()
    confirm = input("Clear all tokens? [y/N] ").strip().lower()
    if confirm == "y":
        count = clear_all_tokens()
        print(f"\nCleared {count} token(s).")
    else:
        print("\nCancelled.")


def status_cli() -> None:
    """CLI entry point for snipara-mcp-status."""
    print()
    print("Snipara MCP - Authentication Status")
    print()

    # Check environment variables
    [REDACTED_SECRET]"SNIPARA_API_KEY")
    project_id = os.environ.get("SNIPARA_PROJECT_ID")

    if [REDACTED_SECRET]"Environment:")
        print(f"  SNIPARA_API_KEY: {api_key[:12]}...")
        if project_id:
            print(f"  SNIPARA_PROJECT_ID: {project_id}")
        print()

    # Check stored tokens
    tokens = load_tokens()
    if tokens:
        print("Stored OAuth Tokens:")
        for pid, data in tokens.items():
            slug = data.get("project_slug", pid)
            expires_at = data.get("expires_at", "unknown")
            try:
                exp_time = datetime.fromisoformat(expires_at)
                if exp_time < datetime.utcnow():
                    status = "EXPIRED"
                else:
                    remaining = exp_time - datetime.utcnow()
                    status = f"valid for {remaining.total_seconds() / 60:.0f} min"
            except (ValueError, TypeError):
                status = "unknown"
            print(f"  - {slug}: {status}")
        print()
    else:
        print("No stored OAuth tokens.")
        print()

    if not api_key and not tokens:
        print("Not authenticated. Run 'snipara-mcp-login' to authenticate.")


if __name__ == "__main__":
    # Allow running as: python -m snipara_mcp.auth login|logout|status
    if len(sys.argv) > 1:
        cmd = sys.argv[1]
        if cmd == "login":
            login_cli()
        elif cmd == "logout":
            logout_cli()
        elif cmd == "status":
            status_cli()
        else:
            print(f"Unknown command: {cmd}")
            print("Usage: python -m snipara_mcp.auth [login|logout|status]")
    else:
        status_cli()


path: src/snipara_mcp/rlm_tools.py
content: """
RLM Runtime tool plugins for Snipara context optimization.

This module provides Snipara tools in a format compatible with rlm-runtime.
When snipara-mcp is installed alongside rlm-runtime, these tools are
automatically registered.

Usage:
    # Automatic (in rlm-runtime)
    rlm = RLM(snipara_api_key="...", snipara_project_slug="...")
    # Tools are auto-registered

    # Manual
    from snipara_mcp.rlm_tools import get_snipara_tools
    tools = get_snipara_tools(api_key="...", project_slug="...")
"""

from __future__ import annotations

import os
from typing import Any, TYPE_CHECKING

import httpx

if TYPE_CHECKING:
    from rlm.backends.base import Tool

# Default API URL - matches the MCP endpoint format
DEFAULT_API_URL = "https://snipara.com"


class SniparaClient:
    """HTTP client for Snipara MCP API."""

    def __init__(
        self,
        api_key: str,
        project_slug: str,
        api_url: str | None = None,
    ):
        """Initialize the Snipara client.

        Args:
            [REDACTED_SECRET] API key (starts with 'rlm_')
            project_slug: Project identifier
            api_url: Optional custom API URL (e.g., https://snipara.com)
        """
        self.[REDACTED_SECRET]
        self.project_slug = project_slug
        self.api_url = api_url or os.environ.get("SNIPARA_API_URL", DEFAULT_API_URL)
        self._client: httpx.AsyncClient | None = None

    async def _get_client(self) -> httpx.AsyncClient:
        """Get or create the HTTP client."""
        if self._client is None or self._client.is_closed:
            self._client = httpx.AsyncClient(
                headers={
                    "X-API-Key": self.api_key,
                    "Content-Type": "application/json",
                },
                timeout=60.0,
            )
        return self._client

    async def call_tool(self, tool_name: str, params: dict[str, Any]) -> dict[str, Any]:
        """Call a Snipara MCP tool.

        Args:
            tool_name: Name of the tool to call
            params: Tool parameters

        Returns:
            Tool result dictionary
        """
        client = await self._get_client()
        response = await client.post(
            f"{self.api_url}/api/mcp/{self.project_slug}",
            json={"tool": tool_name, "params": params},
        )
        response.raise_for_status()
        result = response.json()

        if not result.get("success"):
            raise RuntimeError(result.get("error", "Unknown error"))

        return result.get("result", {})

    async def close(self) -> None:
        """Close the HTTP client."""
        if self._client:
            await self._client.aclose()
            self._client = None


def get_snipara_tools(
    api_key: str | None = None,
    project_slug: str | None = None,
    api_url: str | None = None,
) -> list[Tool]:
    """Get Snipara tools as RLM-compatible Tool objects.

    This function is called by rlm-runtime when snipara-mcp is installed
    and Snipara credentials are configured.

    Args:
        [REDACTED_SECRET] API key (or SNIPARA_API_KEY env var)
        project_slug: Project slug (or SNIPARA_PROJECT_SLUG env var)
        api_url: Optional custom API URL

    Returns:
        List of Tool objects compatible with rlm-runtime

    Raises:
        ImportError: If rlm-runtime is not installed
        ValueError: If credentials are missing
    """
    # Import Tool from rlm-runtime
    try:
        from rlm.backends.base import Tool
    except ImportError:
        raise ImportError(
            "rlm-runtime is required to use Snipara tools. "
            "Install with: pip install rlm-runtime"
        )

    # Get credentials from args or environment
    [REDACTED_SECRET] or os.environ.get("SNIPARA_API_KEY")
    project_slug = project_slug or os.environ.get("SNIPARA_PROJECT_SLUG")

    if not api_key:
        raise ValueError(
            "Snipara API key required. Set SNIPARA_API_KEY or pass api_key parameter."
        )
    if not project_slug:
        raise ValueError(
            "Snipara project slug required. Set SNIPARA_PROJECT_SLUG or pass project_slug parameter."
        )

    # Create client
    client = SniparaClient(api_key, project_slug, api_url)

    # Define tool handlers
    async def context_query(
        query: str,
        max_tokens: int = 4000,
        search_mode: str = "hybrid",
        include_metadata: bool = True,
    ) -> dict[str, Any]:
        """Query Snipara for optimized, relevant context.

        Args:
            query: The question or topic to search for
            max_tokens: Maximum tokens to return (default: 4000)
            search_mode: Search mode - keyword, semantic, or hybrid
            include_metadata: Include file paths and relevance scores

        Returns:
            Dictionary with sections, total_tokens, and suggestions
        """
        return await client.call_tool("rlm_context_query", {
            "query": query,
            "max_tokens": max_tokens,
            "search_mode": search_mode,
            "include_metadata": include_metadata,
        })

    async def sections() -> dict[str, Any]:
        """List all available documentation sections.

        Returns:
            Dictionary with sections list containing paths and chunk counts
        """
        return await client.call_tool("rlm_sections", {})

    async def search(
        pattern: str,
        max_results: int = 20,
    ) -> dict[str, Any]:
        """Search documentation for a regex pattern.

        Args:
            pattern: Regex pattern to search for
            max_results: Maximum number of results

        Returns:
            Dictionary with matches list
        """
        return await client.call_tool("rlm_search", {
            "pattern": pattern,
            "max_results": max_results,
        })

    async def read(
        file: str,
        start_line: int = 1,
        end_line: int | None = None,
    ) -> dict[str, Any]:
        """Read specific lines from a documentation file.

        Args:
            file: File path to read
            start_line: Starting line number (default: 1)
            end_line: Ending line number (optional, reads to end if not specified)

        Returns:
            Dictionary with content string
        """
        params: dict[str, Any] = {"file": file, "start_line": start_line}
        if end_line is not None:
            params["end_line"] = end_line
        return await client.call_tool("rlm_read", params)

    async def shared_context(
        max_tokens: int = 4000,
        categories: list[str] | None = None,
        include_content: bool = True,
    ) -> dict[str, Any]:
        """Get merged context from linked shared collections.

        Returns team best practices, coding standards, and guidelines
        organized by priority category.

        Args:
            max_tokens: Token budget for context
            categories: Filter to specific categories (MANDATORY, BEST_PRACTICES, etc.)
            include_content: Include the actual content

        Returns:
            Dictionary with documents, collections_loaded, and merged_content
        """
        params: dict[str, Any] = {
            "max_tokens": max_tokens,
            "include_content": include_content,
        }
        if categories:
            params["categories"] = categories
        return await client.call_tool("rlm_shared_context", params)

    async def decompose(
        query: str,
        max_depth: int = 2,
    ) -> dict[str, Any]:
        """Break a complex query into sub-queries with execution order.

        Args:
            query: Complex question to decompose
            max_depth: Maximum decomposition depth

        Returns:
            Dictionary with sub_queries and suggested_sequence
        """
        return await client.call_tool("rlm_decompose", {
            "query": query,
            "max_depth": max_depth,
        })

    async def multi_query(
        queries: list[dict[str, str]],
        max_tokens: int = 8000,
    ) -> dict[str, Any]:
        """Execute multiple queries with shared token budget.

        Args:
            queries: List of query objects with 'query' key
            max_tokens: Total token budget

        Returns:
            Dictionary with results for each query
        """
        return await client.call_tool("rlm_multi_query", {
            "queries": queries,
            "max_tokens": max_tokens,
        })

    async def multi_project_query(
        query: str,
        max_tokens: int = 4000,
        per_project_limit: int = 3,
        project_ids: list[str] | None = None,
        exclude_project_ids: list[str] | None = None,
        search_mode: str = "keyword",
        include_metadata: bool = True,
    ) -> dict[str, Any]:
        """Query across multiple projects in your team.

        Args:
            query: The query/question to get context for
            max_tokens: Maximum tokens to return across all projects
            per_project_limit: Maximum sections to return per project
            project_ids: Optional list of project IDs or slugs to include
            exclude_project_ids: Optional list of project IDs or slugs to exclude
            search_mode: Search strategy (keyword, semantic, hybrid)
            include_metadata: Include file paths, line numbers, and relevance scores

        Returns:
            Dictionary with results from multiple projects
        """
        params: dict[str, Any] = {
            "query": query,
            "max_tokens": max_tokens,
            "per_project_limit": per_project_limit,
            "search_mode": search_mode,
            "include_metadata": include_metadata,
        }
        if project_ids:
            params["project_ids"] = project_ids
        if exclude_project_ids:
            params["exclude_project_ids"] = exclude_project_ids
        return await client.call_tool("rlm_multi_project_query", params)

    async def stats() -> dict[str, Any]:
        """Get documentation statistics.

        Returns:
            Dictionary with files_loaded, total_lines, total_characters, sections
        """
        return await client.call_tool("rlm_stats", {})

    async def list_templates(
        category: str | None = None,
    ) -> dict[str, Any]:
        """List available prompt templates from shared collections.

        Args:
            category: Optional category filter

        Returns:
            Dictionary with templates list
        """
        params: dict[str, Any] = {}
        if category:
            params["category"] = category
        return await client.call_tool("rlm_list_templates", params)

    async def get_template(
        template_id: str | None = None,
        slug: str | None = None,
        variables: dict[str, str] | None = None,
    ) -> dict[str, Any]:
        """Get and optionally render a prompt template.

        Args:
            template_id: Template ID
            slug: Template slug (alternative to ID)
            variables: Variables to substitute in template

        Returns:
            Dictionary with template and rendered_prompt
        """
        params: dict[str, Any] = {}
        if template_id:
            params["template_id"] = template_id
        if slug:
            params["slug"] = slug
        if variables:
            params["variables"] = variables
        return await client.call_tool("rlm_get_template", params)

    # ============ SESSION MANAGEMENT TOOLS ============

    async def ask(question: str) -> dict[str, Any]:
        """Basic documentation query.

        Args:
            question: The question to ask about the documentation

        Returns:
            Dictionary with answer
        """
        return await client.call_tool("rlm_ask", {"question": question})

    async def inject(context: str, append: bool = False) -> dict[str, Any]:
        """Inject context into the session.

        Args:
            context: The context to inject
            append: Append to existing context instead of replacing

        Returns:
            Dictionary with success status
        """
        return await client.call_tool("rlm_inject", {
            "context": context,
            "append": append,
        })

    async def get_context() -> dict[str, Any]:
        """Get the current session context.

        Returns:
            Dictionary with current context
        """
        return await client.call_tool("rlm_context", {})

    async def clear_context() -> dict[str, Any]:
        """Clear the current session context.

        Returns:
            Dictionary with success status
        """
        return await client.call_tool("rlm_clear_context", {})

    async def get_settings() -> dict[str, Any]:
        """Get project settings.

        Returns:
            Dictionary with project settings
        """
        return await client.call_tool("rlm_settings", {})

    async def plan(
        query: str,
        strategy: str = "relevance_first",
        max_tokens: int = 16000,
    ) -> dict[str, Any]:
        """Create an execution plan for a complex query.

        Args:
            query: The complex question to plan for
            strategy: Execution strategy (breadth_first, depth_first, relevance_first)
            max_tokens: Total token budget

        Returns:
            Dictionary with plan steps
        """
        return await client.call_tool("rlm_plan", {
            "query": query,
            "strategy": strategy,
            "max_tokens": max_tokens,
        })

    # ============ SUMMARY STORAGE TOOLS ============

    async def store_summary(
        document_path: str,
        summary: str,
        summary_type: str = "concise",
        section_id: str | None = None,
        line_start: int | None = None,
        line_end: int | None = None,
        generated_by: str | None = None,
    ) -> dict[str, Any]:
        """Store a summary for a document or section.

        Args:
            document_path: Path to the document
            summary: The summary text to store
            summary_type: Type of summary (concise, detailed, technical, keywords, custom)
            section_id: Optional section identifier
            line_start: Start line for section summary
            line_end: End line for section summary
            generated_by: Model that generated the summary

        Returns:
            Dictionary with summary_id and status
        """
        params: dict[str, Any] = {
            "document_path": document_path,
            "summary": summary,
            "summary_type": summary_type,
        }
        if section_id:
            params["section_id"] = section_id
        if line_start is not None:
            params["line_start"] = line_start
        if line_end is not None:
            params["line_end"] = line_end
        if generated_by:
            params["generated_by"] = generated_by
        return await client.call_tool("rlm_store_summary", params)

    async def get_summaries(
        document_path: str | None = None,
        summary_type: str | None = None,
        section_id: str | None = None,
        include_content: bool = True,
    ) -> dict[str, Any]:
        """Get stored summaries.

        Args:
            document_path: Filter by document path
            summary_type: Filter by summary type
            section_id: Filter by section ID
            include_content: Include summary content in response

        Returns:
            Dictionary with summaries list
        """
        params: dict[str, Any] = {"include_content": include_content}
        if document_path:
            params["document_path"] = document_path
        if summary_type:
            params["summary_type"] = summary_type
        if section_id:
            params["section_id"] = section_id
        return await client.call_tool("rlm_get_summaries", params)

    async def delete_summary(
        summary_id: str | None = None,
        document_path: str | None = None,
        summary_type: str | None = None,
    ) -> dict[str, Any]:
        """Delete stored summaries.

        Args:
            summary_id: Specific summary ID to delete
            document_path: Delete all summaries for document
            summary_type: Delete summaries of this type

        Returns:
            Dictionary with deleted count
        """
        params: dict[str, Any] = {}
        if summary_id:
            params["summary_id"] = summary_id
        if document_path:
            params["document_path"] = document_path
        if summary_type:
            params["summary_type"] = summary_type
        return await client.call_tool("rlm_delete_summary", params)

    # ============ AGENT MEMORY TOOLS ============

    async def remember(
        content: str,
        type: str = "fact",
        scope: str = "project",
        category: str | None = None,
        ttl_days: int | None = None,
        related_to: list[str] | None = None,
        document_refs: list[str] | None = None,
        source: str | None = None,
    ) -> dict[str, Any]:
        """Store a memory.

        Args:
            content: The memory content to store
            type: Memory type (fact, decision, learning, preference, todo, context)
            scope: Visibility scope (agent, project, team, user)
            category: Optional grouping category
            ttl_days: Days until memory expires (null = permanent)
            related_to: IDs of related memories
            document_refs: Referenced document paths
            source: What created this memory

        Returns:
            Dictionary with memory_id and status
        """
        params: dict[str, Any] = {
            "content": content,
            "type": type,
            "scope": scope,
        }
        if category:
            params["category"] = category
        if ttl_days is not None:
            params["ttl_days"] = ttl_days
        if related_to:
            params["related_to"] = related_to
        if document_refs:
            params["document_refs"] = document_refs
        if source:
            params["source"] = source
        return await client.call_tool("rlm_remember", params)

    async def recall(
        query: str,
        type: str | None = None,
        scope: str | None = None,
        category: str | None = None,
        limit: int = 5,
        min_relevance: float = 0.5,
        include_expired: bool = False,
    ) -> dict[str, Any]:
        """Recall memories semantically.

        Args:
            query: Search query for semantic recall
            type: Filter by memory type
            scope: Filter by scope
            category: Filter by category
            limit: Maximum memories to return
            min_relevance: Minimum relevance score (0-1)
            include_expired: Include expired memories

        Returns:
            Dictionary with recalled memories
        """
        params: dict[str, Any] = {
            "query": query,
            "limit": limit,
            "min_relevance": min_relevance,
            "include_expired": include_expired,
        }
        if type:
            params["type"] = type
        if scope:
            params["scope"] = scope
        if category:
            params["category"] = category
        return await client.call_tool("rlm_recall", params)

    async def memories(
        type: str | None = None,
        scope: str | None = None,
        category: str | None = None,
        search: str | None = None,
        limit: int = 20,
        offset: int = 0,
        include_expired: bool = False,
    ) -> dict[str, Any]:
        """List stored memories.

        Args:
            type: Filter by memory type
            scope: Filter by scope
            category: Filter by category
            search: Text search in content
            limit: Maximum memories to return
            offset: Offset for pagination
            include_expired: Include expired memories

        Returns:
            Dictionary with memories list
        """
        params: dict[str, Any] = {
            "limit": limit,
            "offset": offset,
            "include_expired": include_expired,
        }
        if type:
            params["type"] = type
        if scope:
            params["scope"] = scope
        if category:
            params["category"] = category
        if search:
            params["search"] = search
        return await client.call_tool("rlm_memories", params)

    async def forget(
        memory_id: str | None = None,
        type: str | None = None,
        category: str | None = None,
        older_than_days: int | None = None,
    ) -> dict[str, Any]:
        """Delete memories.

        Args:
            memory_id: Specific memory ID to delete
            type: Delete all memories of this type
            category: Delete all memories in this category
            older_than_days: Delete memories older than N days

        Returns:
            Dictionary with deleted count
        """
        params: dict[str, Any] = {}
        if memory_id:
            params["memory_id"] = memory_id
        if type:
            params["type"] = type
        if category:
            params["category"] = category
        if older_than_days is not None:
            params["older_than_days"] = older_than_days
        return await client.call_tool("rlm_forget", params)

    # ============ SWARM COORDINATION TOOLS ============

    async def swarm_create(
        name: str,
        description: str | None = None,
        max_agents: int = 10,
        task_timeout: int = 300,
        claim_timeout: int = 600,
    ) -> dict[str, Any]:
        """Create a new agent swarm.

        Args:
            name: Swarm name
            description: Swarm description
            max_agents: Maximum agents allowed (2-50)
            task_timeout: Task timeout in seconds
            claim_timeout: Resource claim timeout in seconds

        Returns:
            Dictionary with swarm_id
        """
        params: dict[str, Any] = {
            "name": name,
            "max_agents": max_agents,
            "task_timeout": task_timeout,
            "claim_timeout": claim_timeout,
        }
        if description:
            params["description"] = description
        return await client.call_tool("rlm_swarm_create", params)

    async def swarm_join(
        swarm_id: str,
        agent_id: str,
        name: str | None = None,
    ) -> dict[str, Any]:
        """Join an existing swarm.

        Args:
            swarm_id: ID of swarm to join
            agent_id: Unique identifier for this agent
            name: Human-readable agent name

        Returns:
            Dictionary with join status
        """
        params: dict[str, Any] = {
            "swarm_id": swarm_id,
            "agent_id": agent_id,
        }
        if name:
            params["name"] = name
        return await client.call_tool("rlm_swarm_join", params)

    async def claim(
        swarm_id: str,
        agent_id: str,
        resource_type: str,
        resource_id: str,
        ttl_seconds: int | None = None,
    ) -> dict[str, Any]:
        """Claim exclusive access to a resource.

        Args:
            swarm_id: Swarm ID
            agent_id: Agent ID making the claim
            resource_type: Resource type (file, function, module, custom)
            resource_id: Resource identifier (e.g., 'src/auth.ts')
            ttl_seconds: Custom TTL (uses swarm default if null)

        Returns:
            Dictionary with claim status
        """
        params: dict[str, Any] = {
            "swarm_id": swarm_id,
            "agent_id": agent_id,
            "resource_type": resource_type,
            "resource_id": resource_id,
        }
        if ttl_seconds is not None:
            params["ttl_seconds"] = ttl_seconds
        return await client.call_tool("rlm_claim", params)

    async def release(
        swarm_id: str,
        agent_id: str,
        resource_type: str,
        resource_id: str,
    ) -> dict[str, Any]:
        """Release a claimed resource.

        Args:
            swarm_id: Swarm ID
            agent_id: Agent ID releasing the claim
            resource_type: Resource type
            resource_id: Resource identifier

        Returns:
            Dictionary with release status
        """
        return await client.call_tool("rlm_release", {
            "swarm_id": swarm_id,
            "agent_id": agent_id,
            "resource_type": resource_type,
            "resource_id": resource_id,
        })

    async def state_get(swarm_id: str, key: str) -> dict[str, Any]:
        """Get shared swarm state.

        Args:
            swarm_id: Swarm ID
            key: State key to retrieve

        Returns:
            Dictionary with state value
        """
        return await client.call_tool("rlm_state_get", {
            "swarm_id": swarm_id,
            "key": key,
        })

    async def state_set(
        swarm_id: str,
        agent_id: str,
        key: str,
        value: Any,
        expected_version: int | None = None,
    ) -> dict[str, Any]:
        """Set shared swarm state.

        Args:
            swarm_id: Swarm ID
            agent_id: Agent ID setting the state
            key: State key
            value: State value (JSON-serializable)
            expected_version: Expected version for optimistic locking

        Returns:
            Dictionary with set status
        """
        params: dict[str, Any] = {
            "swarm_id": swarm_id,
            "agent_id": agent_id,
            "key": key,
            "value": value,
        }
        if expected_version is not None:
            params["expected_version"] = expected_version
        return await client.call_tool("rlm_state_set", params)

    async def broadcast(
        swarm_id: str,
        agent_id: str,
        event_type: str,
        payload: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        """Broadcast an event to the swarm.

        Args:
            swarm_id: Swarm ID
            agent_id: Agent ID broadcasting
            event_type: Event type (e.g., 'file_changed', 'task_done')
            payload: Event payload

        Returns:
            Dictionary with broadcast status
        """
        return await client.call_tool("rlm_broadcast", {
            "swarm_id": swarm_id,
            "agent_id": agent_id,
            "event_type": event_type,
            "payload": payload or {},
        })

    async def task_create(
        swarm_id: str,
        title: str,
        description: str | None = None,
        priority: int = 0,
        depends_on: list[str] | None = None,
    ) -> dict[str, Any]:
        """Create a task in the swarm.

        Args:
            swarm_id: Swarm ID
            title: Task title
            description: Task description
            priority: Priority (higher = more urgent)
            depends_on: Task IDs that must complete first

        Returns:
            Dictionary with task_id
        """
        params: dict[str, Any] = {
            "swarm_id": swarm_id,
            "title": title,
            "priority": priority,
        }
        if description:
            params["description"] = description
        if depends_on:
            params["depends_on"] = depends_on
        return await client.call_tool("rlm_task_create", params)

    async def task_claim(
        swarm_id: str,
        agent_id: str,
        task_id: str | None = None,
    ) -> dict[str, Any]:
        """Claim a task from the swarm queue.

        Args:
            swarm_id: Swarm ID
            agent_id: Agent ID claiming
            task_id: Specific task ID (null = get next available)

        Returns:
            Dictionary with claimed task
        """
        params: dict[str, Any] = {
            "swarm_id": swarm_id,
            "agent_id": agent_id,
        }
        if task_id:
            params["task_id"] = task_id
        return await client.call_tool("rlm_task_claim", params)

    async def task_complete(
        swarm_id: str,
        agent_id: str,
        task_id: str,
        result: dict[str, Any] | None = None,
        error: str | None = None,
    ) -> dict[str, Any]:
        """Complete a task.

        Args:
            swarm_id: Swarm ID
            agent_id: Agent ID completing
            task_id: Task ID to complete
            result: Task result data
            error: Error message if task failed

        Returns:
            Dictionary with completion status
        """
        params: dict[str, Any] = {
            "swarm_id": swarm_id,
            "agent_id": agent_id,
            "task_id": task_id,
        }
        if result:
            params["result"] = result
        if error:
            params["error"] = error
        return await client.call_tool("rlm_task_complete", params)

    # ============ DOCUMENT MANAGEMENT TOOLS ============

    async def upload_document(
        path: str,
        content: str,
    ) -> dict[str, Any]:
        """Upload a document.

        Args:
            path: Document path
            content: Document content

        Returns:
            Dictionary with upload status
        """
        return await client.call_tool("rlm_upload_document", {
            "path": path,
            "content": content,
        })

    async def sync_documents(
        documents: list[dict[str, str]],
        delete_missing: bool = False,
    ) -> dict[str, Any]:
        """Sync multiple documents.

        Args:
            documents: List of documents with path and content
            delete_missing: Delete documents not in list

        Returns:
            Dictionary with sync stats
        """
        return await client.call_tool("rlm_sync_documents", {
            "documents": documents,
            "delete_missing": delete_missing,
        })

    # Build and return tool list
    return [
        Tool(
            name="context_query",
            description=(
                "Query Snipara for optimized, relevant context. "
                "Returns the most relevant documentation sections within your token budget. "
                "This is the PRIMARY tool for any documentation questions. "
                "Uses hybrid search (keyword + semantic) by default for best results."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "The question or topic to search for",
                    },
                    "max_tokens": {
                        "type": "integer",
                        "default": 4000,
                        "description": "Maximum tokens to return",
                    },
                    "search_mode": {
                        "type": "string",
                        "enum": ["keyword", "semantic", "hybrid"],
                        "default": "hybrid",
                        "description": "Search mode",
                    },
                    "include_metadata": {
                        "type": "boolean",
                        "default": True,
                        "description": "Include file paths and relevance scores",
                    },
                },
                "required": ["query"],
            },
            handler=context_query,
        ),
        Tool(
            name="sections",
            description=(
                "List all available documentation sections in the project. "
                "Returns file paths and chunk counts for each document."
            ),
            parameters={
                "type": "object",
                "properties": {},
            },
            handler=sections,
        ),
        Tool(
            name="search",
            description=(
                "Search documentation for a regex pattern. "
                "Returns matching lines with file paths and line numbers."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "pattern": {
                        "type": "string",
                        "description": "Regex pattern to search for",
                    },
                    "max_results": {
                        "type": "integer",
                        "default": 20,
                        "description": "Maximum number of results",
                    },
                },
                "required": ["pattern"],
            },
            handler=search,
        ),
        Tool(
            name="read",
            description=(
                "Read specific lines from a documentation file. "
                "Use after search or sections to get full content."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "file": {
                        "type": "string",
                        "description": "File path to read",
                    },
                    "start_line": {
                        "type": "integer",
                        "default": 1,
                        "description": "Starting line number (default: 1)",
                    },
                    "end_line": {
                        "type": "integer",
                        "description": "Ending line number (optional)",
                    },
                },
                "required": ["file"],
            },
            handler=read,
        ),
        Tool(
            name="shared_context",
            description=(
                "Get merged context from linked shared collections. "
                "Returns team best practices, coding standards, and guidelines "
                "organized by priority (MANDATORY > BEST_PRACTICES > GUIDELINES > REFERENCE). "
                "Use this to ensure code follows team conventions."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "max_tokens": {
                        "type": "integer",
                        "default": 4000,
                        "description": "Token budget for context",
                    },
                    "categories": {
                        "type": "array",
                        "items": {
                            "type": "string",
                            "enum": ["MANDATORY", "BEST_PRACTICES", "GUIDELINES", "REFERENCE"],
                        },
                        "description": "Filter to specific categories",
                    },
                    "include_content": {
                        "type": "boolean",
                        "default": True,
                        "description": "Include the actual content",
                    },
                },
            },
            handler=shared_context,
        ),
        Tool(
            name="decompose",
            description=(
                "Break a complex query into sub-queries with execution order. "
                "Use for multi-part questions that need to be answered step by step."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "Complex question to decompose",
                    },
                    "max_depth": {
                        "type": "integer",
                        "default": 2,
                        "description": "Maximum decomposition depth",
                    },
                },
                "required": ["query"],
            },
            handler=decompose,
        ),
        Tool(
            name="multi_query",
            description=(
                "Execute multiple queries with shared token budget. "
                "More efficient than calling context_query multiple times."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "queries": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "query": {"type": "string"},
                            },
                            "required": ["query"],
                        },
                        "description": "List of queries to execute",
                    },
                    "max_tokens": {
                        "type": "integer",
                        "default": 8000,
                        "description": "Total token budget",
                    },
                },
                "required": ["queries"],
            },
            handler=multi_query,
        ),
        Tool(
            name="multi_project_query",
            description=(
                "Query across multiple projects in your team. "
                "Useful for finding context across related codebases or shared documentation."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "The query/question to get context for",
                    },
                    "max_tokens": {
                        "type": "integer",
                        "default": 4000,
                        "description": "Maximum tokens to return across all projects",
                    },
                    "per_project_limit": {
                        "type": "integer",
                        "default": 3,
                        "description": "Maximum sections to return per project",
                    },
                    "project_ids": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Optional list of project IDs or slugs to include",
                    },
                    "exclude_project_ids": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Optional list of project IDs or slugs to exclude",
                    },
                    "search_mode": {
                        "type": "string",
                        "enum": ["keyword", "semantic", "hybrid"],
                        "default": "keyword",
                        "description": "Search strategy",
                    },
                    "include_metadata": {
                        "type": "boolean",
                        "default": True,
                        "description": "Include file paths, line numbers, and relevance scores",
                    },
                },
                "required": ["query"],
            },
            handler=multi_project_query,
        ),
        Tool(
            name="stats",
            description=(
                "Get documentation statistics. "
                "Returns file count, line count, and section count."
            ),
            parameters={
                "type": "object",
                "properties": {},
            },
            handler=stats,
        ),
        Tool(
            name="list_templates",
            description=(
                "List available prompt templates from shared collections. "
                "Templates can be used with get_template to generate prompts."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "category": {
                        "type": "string",
                        "description": "Filter by category",
                    },
                },
            },
            handler=list_templates,
        ),
        Tool(
            name="get_template",
            description=(
                "Get and optionally render a prompt template. "
                "Pass variables to substitute placeholders in the template."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "template_id": {
                        "type": "string",
                        "description": "Template ID",
                    },
                    "slug": {
                        "type": "string",
                        "description": "Template slug (alternative to ID)",
                    },
                    "variables": {
                        "type": "object",
                        "additionalProperties": {"type": "string"},
                        "description": "Variables to substitute",
                    },
                },
            },
            handler=get_template,
        ),
        # ============ SESSION MANAGEMENT TOOLS ============
        Tool(
            name="ask",
            description=(
                "Basic documentation query. "
                "Ask a question and get an answer based on the documentation."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "question": {
                        "type": "string",
                        "description": "The question to ask about the documentation",
                    },
                },
                "required": ["question"],
            },
            handler=ask,
        ),
        Tool(
            name="inject",
            description=(
                "Inject context into the session. "
                "This context will be included in subsequent queries."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "context": {
                        "type": "string",
                        "description": "The context to inject",
                    },
                    "append": {
                        "type": "boolean",
                        "default": False,
                        "description": "Append to existing context instead of replacing",
                    },
                },
                "required": ["context"],
            },
            handler=inject,
        ),
        Tool(
            name="context",
            description=(
                "Get the current session context. "
                "Returns any context that was previously injected."
            ),
            parameters={
                "type": "object",
                "properties": {},
            },
            handler=get_context,
        ),
        Tool(
            name="clear_context",
            description=(
                "Clear the current session context. "
                "Removes any previously injected context."
            ),
            parameters={
                "type": "object",
                "properties": {},
            },
            handler=clear_context,
        ),
        Tool(
            name="settings",
            description=(
                "Get project settings. "
                "Returns configuration like max tokens, search mode, etc."
            ),
            parameters={
                "type": "object",
                "properties": {},
            },
            handler=get_settings,
        ),
        Tool(
            name="plan",
            description=(
                "Create an execution plan for a complex query. "
                "Returns a sequence of steps to answer complex multi-part questions."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "The complex question to plan for",
                    },
                    "strategy": {
                        "type": "string",
                        "enum": ["breadth_first", "depth_first", "relevance_first"],
                        "default": "relevance_first",
                        "description": "Execution strategy",
                    },
                    "max_tokens": {
                        "type": "integer",
                        "default": 16000,
                        "description": "Total token budget",
                    },
                },
                "required": ["query"],
            },
            handler=plan,
        ),
        # ============ SUMMARY STORAGE TOOLS ============
        Tool(
            name="store_summary",
            description=(
                "Store a summary for a document or section. "
                "Summaries are used to provide concise context in queries."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "document_path": {
                        "type": "string",
                        "description": "Path to the document",
                    },
                    "summary": {
                        "type": "string",
                        "description": "The summary text to store",
                    },
                    "summary_type": {
                        "type": "string",
                        "enum": ["concise", "detailed", "technical", "keywords", "custom"],
                        "default": "concise",
                        "description": "Type of summary",
                    },
                    "section_id": {
                        "type": "string",
                        "description": "Optional section identifier",
                    },
                    "line_start": {
                        "type": "integer",
                        "description": "Start line for section summary",
                    },
                    "line_end": {
                        "type": "integer",
                        "description": "End line for section summary",
                    },
                    "generated_by": {
                        "type": "string",
                        "description": "Model that generated the summary",
                    },
                },
                "required": ["document_path", "summary"],
            },
            handler=store_summary,
        ),
        Tool(
            name="get_summaries",
            description=(
                "Get stored summaries. "
                "Filter by document path, type, or section."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "document_path": {
                        "type": "string",
                        "description": "Filter by document path",
                    },
                    "summary_type": {
                        "type": "string",
                        "enum": ["concise", "detailed", "technical", "keywords", "custom"],
                        "description": "Filter by summary type",
                    },
                    "section_id": {
                        "type": "string",
                        "description": "Filter by section ID",
                    },
                    "include_content": {
                        "type": "boolean",
                        "default": True,
                        "description": "Include summary content in response",
                    },
                },
            },
            handler=get_summaries,
        ),
        Tool(
            name="delete_summary",
            description=(
                "Delete stored summaries. "
                "Delete by ID, document path, or type."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "summary_id": {
                        "type": "string",
                        "description": "Specific summary ID to delete",
                    },
                    "document_path": {
                        "type": "string",
                        "description": "Delete all summaries for document",
                    },
                    "summary_type": {
                        "type": "string",
                        "enum": ["concise", "detailed", "technical", "keywords", "custom"],
                        "description": "Delete summaries of this type",
                    },
                },
            },
            handler=delete_summary,
        ),
        # ============ AGENT MEMORY TOOLS ============
        Tool(
            name="remember",
            description=(
                "Store a memory for later recall. "
                "Memories can be facts, decisions, learnings, preferences, todos, or context. "
                "Use this to persist important information across sessions."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "content": {
                        "type": "string",
                        "description": "The memory content to store",
                    },
                    "type": {
                        "type": "string",
                        "enum": ["fact", "decision", "learning", "preference", "todo", "context"],
                        "default": "fact",
                        "description": "Type of memory",
                    },
                    "scope": {
                        "type": "string",
                        "enum": ["agent", "project", "team", "user"],
                        "default": "project",
                        "description": "Visibility scope",
                    },
                    "category": {
                        "type": "string",
                        "description": "Optional grouping category",
                    },
                    "ttl_days": {
                        "type": "integer",
                        "description": "Days until memory expires (null = permanent)",
                    },
                    "related_to": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "IDs of related memories",
                    },
                    "document_refs": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Referenced document paths",
                    },
                    "source": {
                        "type": "string",
                        "description": "What created this memory",
                    },
                },
                "required": ["content"],
            },
            handler=remember,
        ),
        Tool(
            name="recall",
            description=(
                "Recall memories semantically. "
                "Search for relevant memories using natural language queries."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "Search query for semantic recall",
                    },
                    "type": {
                        "type": "string",
                        "enum": ["fact", "decision", "learning", "preference", "todo", "context"],
                        "description": "Filter by memory type",
                    },
                    "scope": {
                        "type": "string",
                        "enum": ["agent", "project", "team", "user"],
                        "description": "Filter by scope",
                    },
                    "category": {
                        "type": "string",
                        "description": "Filter by category",
                    },
                    "limit": {
                        "type": "integer",
                        "default": 5,
                        "description": "Maximum memories to return",
                    },
                    "min_relevance": {
                        "type": "number",
                        "default": 0.5,
                        "description": "Minimum relevance score (0-1)",
                    },
                    "include_expired": {
                        "type": "boolean",
                        "default": False,
                        "description": "Include expired memories",
                    },
                },
                "required": ["query"],
            },
            handler=recall,
        ),
        Tool(
            name="memories",
            description=(
                "List stored memories with filters. "
                "Browse memories by type, scope, category, or search text."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "type": {
                        "type": "string",
                        "enum": ["fact", "decision", "learning", "preference", "todo", "context"],
                        "description": "Filter by memory type",
                    },
                    "scope": {
                        "type": "string",
                        "enum": ["agent", "project", "team", "user"],
                        "description": "Filter by scope",
                    },
                    "category": {
                        "type": "string",
                        "description": "Filter by category",
                    },
                    "search": {
                        "type": "string",
                        "description": "Text search in content",
                    },
                    "limit": {
                        "type": "integer",
                        "default": 20,
                        "description": "Maximum memories to return",
                    },
                    "offset": {
                        "type": "integer",
                        "default": 0,
                        "description": "Offset for pagination",
                    },
                    "include_expired": {
                        "type": "boolean",
                        "default": False,
                        "description": "Include expired memories",
                    },
                },
            },
            handler=memories,
        ),
        Tool(
            name="forget",
            description=(
                "Delete memories. "
                "Remove memories by ID, type, category, or age."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "memory_id": {
                        "type": "string",
                        "description": "Specific memory ID to delete",
                    },
                    "type": {
                        "type": "string",
                        "enum": ["fact", "decision", "learning", "preference", "todo", "context"],
                        "description": "Delete all memories of this type",
                    },
                    "category": {
                        "type": "string",
                        "description": "Delete all memories in this category",
                    },
                    "older_than_days": {
                        "type": "integer",
                        "description": "Delete memories older than N days",
                    },
                },
            },
            handler=forget,
        ),
        # ============ SWARM COORDINATION TOOLS ============
        Tool(
            name="swarm_create",
            description=(
                "Create a new agent swarm. "
                "Swarms allow multiple agents to coordinate on a shared task."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string",
                        "description": "Swarm name",
                    },
                    "description": {
                        "type": "string",
                        "description": "Swarm description",
                    },
                    "max_agents": {
                        "type": "integer",
                        "default": 10,
                        "description": "Maximum agents allowed (2-50)",
                    },
                    "task_timeout": {
                        "type": "integer",
                        "default": 300,
                        "description": "Task timeout in seconds",
                    },
                    "claim_timeout": {
                        "type": "integer",
                        "default": 600,
                        "description": "Resource claim timeout in seconds",
                    },
                },
                "required": ["name"],
            },
            handler=swarm_create,
        ),
        Tool(
            name="swarm_join",
            description=(
                "Join an existing swarm. "
                "Agents must join a swarm before claiming resources or tasks."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "swarm_id": {
                        "type": "string",
                        "description": "ID of swarm to join",
                    },
                    "agent_id": {
                        "type": "string",
                        "description": "Unique identifier for this agent",
                    },
                    "name": {
                        "type": "string",
                        "description": "Human-readable agent name",
                    },
                },
                "required": ["swarm_id", "agent_id"],
            },
            handler=swarm_join,
        ),
        Tool(
            name="claim",
            description=(
                "Claim exclusive access to a resource. "
                "Prevents other agents from modifying the same file/function."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "swarm_id": {
                        "type": "string",
                        "description": "Swarm ID",
                    },
                    "agent_id": {
                        "type": "string",
                        "description": "Agent ID making the claim",
                    },
                    "resource_type": {
                        "type": "string",
                        "description": "Resource type (file, function, module, custom)",
                    },
                    "resource_id": {
                        "type": "string",
                        "description": "Resource identifier (e.g., 'src/auth.ts')",
                    },
                    "ttl_seconds": {
                        "type": "integer",
                        "description": "Custom TTL (uses swarm default if null)",
                    },
                },
                "required": ["swarm_id", "agent_id", "resource_type", "resource_id"],
            },
            handler=claim,
        ),
        Tool(
            name="release",
            description=(
                "Release a claimed resource. "
                "Allows other agents to claim the resource."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "swarm_id": {
                        "type": "string",
                        "description": "Swarm ID",
                    },
                    "agent_id": {
                        "type": "string",
                        "description": "Agent ID releasing the claim",
                    },
                    "resource_type": {
                        "type": "string",
                        "description": "Resource type",
                    },
                    "resource_id": {
                        "type": "string",
                        "description": "Resource identifier",
                    },
                },
                "required": ["swarm_id", "agent_id", "resource_type", "resource_id"],
            },
            handler=release,
        ),
        Tool(
            name="state_get",
            description=(
                "Get shared swarm state. "
                "Retrieve a value from the shared state store."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "swarm_id": {
                        "type": "string",
                        "description": "Swarm ID",
                    },
                    "key": {
                        "type": "string",
                        "description": "State key to retrieve",
                    },
                },
                "required": ["swarm_id", "key"],
            },
            handler=state_get,
        ),
        Tool(
            name="state_set",
            description=(
                "Set shared swarm state. "
                "Store a value in the shared state store with optional optimistic locking."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "swarm_id": {
                        "type": "string",
                        "description": "Swarm ID",
                    },
                    "agent_id": {
                        "type": "string",
                        "description": "Agent ID setting the state",
                    },
                    "key": {
                        "type": "string",
                        "description": "State key",
                    },
                    "value": {
                        "description": "State value (JSON-serializable)",
                    },
                    "expected_version": {
                        "type": "integer",
                        "description": "Expected version for optimistic locking",
                    },
                },
                "required": ["swarm_id", "agent_id", "key", "value"],
            },
            handler=state_set,
        ),
        Tool(
            name="broadcast",
            description=(
                "Broadcast an event to the swarm. "
                "Notify other agents about changes or completions."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "swarm_id": {
                        "type": "string",
                        "description": "Swarm ID",
                    },
                    "agent_id": {
                        "type": "string",
                        "description": "Agent ID broadcasting",
                    },
                    "event_type": {
                        "type": "string",
                        "description": "Event type (e.g., 'file_changed', 'task_done')",
                    },
                    "payload": {
                        "type": "object",
                        "description": "Event payload",
                    },
                },
                "required": ["swarm_id", "agent_id", "event_type"],
            },
            handler=broadcast,
        ),
        Tool(
            name="task_create",
            description=(
                "Create a task in the swarm. "
                "Tasks can be claimed and completed by agents."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "swarm_id": {
                        "type": "string",
                        "description": "Swarm ID",
                    },
                    "title": {
                        "type": "string",
                        "description": "Task title",
                    },
                    "description": {
                        "type": "string",
                        "description": "Task description",
                    },
                    "priority": {
                        "type": "integer",
                        "default": 0,
                        "description": "Priority (higher = more urgent)",
                    },
                    "depends_on": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Task IDs that must complete first",
                    },
                },
                "required": ["swarm_id", "title"],
            },
            handler=task_create,
        ),
        Tool(
            name="task_claim",
            description=(
                "Claim a task from the swarm queue. "
                "Returns the next available task or a specific task."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "swarm_id": {
                        "type": "string",
                        "description": "Swarm ID",
                    },
                    "agent_id": {
                        "type": "string",
                        "description": "Agent ID claiming",
                    },
                    "task_id": {
                        "type": "string",
                        "description": "Specific task ID (null = get next available)",
                    },
                },
                "required": ["swarm_id", "agent_id"],
            },
            handler=task_claim,
        ),
        Tool(
            name="task_complete",
            description=(
                "Complete a task. "
                "Mark a claimed task as done with optional result or error."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "swarm_id": {
                        "type": "string",
                        "description": "Swarm ID",
                    },
                    "agent_id": {
                        "type": "string",
                        "description": "Agent ID completing",
                    },
                    "task_id": {
                        "type": "string",
                        "description": "Task ID to complete",
                    },
                    "result": {
                        "type": "object",
                        "description": "Task result data",
                    },
                    "error": {
                        "type": "string",
                        "description": "Error message if task failed",
                    },
                },
                "required": ["swarm_id", "agent_id", "task_id"],
            },
            handler=task_complete,
        ),
        # ============ DOCUMENT MANAGEMENT TOOLS ============
        Tool(
            name="upload_document",
            description=(
                "Upload a document to the project. "
                "Creates or updates a document in the documentation store."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "Document path",
                    },
                    "content": {
                        "type": "string",
                        "description": "Document content",
                    },
                },
                "required": ["path", "content"],
            },
            handler=upload_document,
        ),
        Tool(
            name="sync_documents",
            description=(
                "Sync multiple documents. "
                "Bulk create/update/delete documents to match provided list."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "documents": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "path": {"type": "string"},
                                "content": {"type": "string"},
                            },
                            "required": ["path", "content"],
                        },
                        "description": "List of documents with path and content",
                    },
                    "delete_missing": {
                        "type": "boolean",
                        "default": False,
                        "description": "Delete documents not in list",
                    },
                },
                "required": ["documents"],
            },
            handler=sync_documents,
        ),
    ]


path: src/snipara_mcp/server.py
content: #!/usr/bin/env python3
"""
Snipara MCP Server - stdio transport to Snipara REST API.

This MCP server connects your LLM client (Claude Desktop, Cursor, etc.)
to your Snipara project for context-optimized documentation queries.

Usage:
    snipara-mcp

Authentication (in priority order):
    1. OAuth token from ~/.snipara/tokens.json (run: snipara-mcp-login)
    2. SNIPARA_API_KEY environment variable (legacy)

Environment variables:
    SNIPARA_PROJECT_ID: Your project ID (required if not using OAuth)
    SNIPARA_API_KEY: Your Snipara API key (legacy, optional if using OAuth)
    SNIPARA_API_URL: API URL (default: https://api.snipara.com)
    SNIPARA_IGNORE_OAUTH: Set to "true" to skip OAuth and use API key instead
"""

import asyncio
import os
import sys
import time
from typing import Any

import httpx
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import TextContent, Tool

# Configuration
API_URL = os.environ.get("SNIPARA_API_URL", "https://api.snipara.com")

# Auth state (loaded on startup)
_auth_token: str | None = None
_auth_type: str = "none"  # "oauth", "api_key", or "none"
_project_id: str | None = None


def _load_auth() -> tuple[str | None, str, str | None]:
    """
    Load authentication credentials in priority order:
    1. OAuth token from ~/.snipara/tokens.json (unless SNIPARA_IGNORE_OAUTH=true)
    2. API key from environment

    Returns:
        Tuple of (token, auth_type, project_id)
    """
    # Check if OAuth should be skipped
    ignore_oauth = os.environ.get("SNIPARA_IGNORE_OAUTH", "").lower() in ("true", "1", "yes")

    # Try OAuth tokens first (unless ignored)
    if not ignore_oauth:
        try:
            from .auth import load_tokens
            tokens = load_tokens()
            if tokens:
                # If PROJECT_ID is set, use that project's token
                env_project_id = os.environ.get("SNIPARA_PROJECT_ID")
                if env_project_id and env_project_id in tokens:
                    token_data = tokens[env_project_id]
                    access_token = token_data.get("access_token")
                    if access_token:
                        return access_token, "oauth", env_project_id
                # Otherwise, use the first available token
                for project_id, token_data in tokens.items():
                    access_token = token_data.get("access_token")
                    if access_token:
                        return access_token, "oauth", project_id
        except ImportError:
            pass  # auth module not available
        except Exception:
            pass  # Token loading failed

    # Fall back to API key
    [REDACTED_SECRET]"SNIPARA_API_KEY", "")
    project_id = os.environ.get("SNIPARA_PROJECT_ID", "")
    if api_key and project_id:
        return api_key, "api_key", project_id

    return None, "none", project_id or None


# Load auth on module import
_auth_token, _auth_type, _project_id = _load_auth()

# Legacy compatibility
[REDACTED_SECRET] or os.environ.get("SNIPARA_API_KEY", "")
PROJECT_ID = _project_id or os.environ.get("SNIPARA_PROJECT_ID", "")

# Session context cache
_session_context: str = ""

# Settings cache (5 minute TTL)
_settings_cache: dict[str, Any] = {}
_settings_cache_time: float = 0
SETTINGS_CACHE_TTL = 300  # 5 minutes


async def get_project_settings() -> dict[str, Any]:
    """Fetch project automation settings from API with caching."""
    global _settings_cache, _settings_cache_time

    # Return cached settings if still valid
    if _settings_cache and (time.time() - _settings_cache_time) < SETTINGS_CACHE_TTL:
        return _settings_cache

    # Fetch fresh settings from API
    try:
        async with httpx.AsyncClient(timeout=10.0) as client:
            response = await client.get(
                f"{API_URL}/v1/{PROJECT_ID}/automation",
                headers=get_headers(),
            )
            if response.status_code == 200:
                data = response.json()
                _settings_cache = data.get("settings", {})
                _settings_cache_time = time.time()
                return _settings_cache
    except Exception:
        pass  # Fall back to defaults on error

    # Return defaults if API fails
    return {
        "maxTokensPerQuery": 4000,
        "searchMode": "hybrid",
        "includeSummaries": True,
        "autoInjectContext": False,
        "enrichPrompts": False,
    }

server = Server("snipara")


def get_headers() -> dict[str, str]:
    """Get request headers with appropriate auth."""
    headers = {"Content-Type": "application/json"}

    if _auth_type == "oauth" and _auth_token:
        headers["Authorization"] = f"Bearer {_auth_token}"
    elif _auth_type == "api_key" and _auth_token:
        headers["X-API-Key"] = _auth_token
    elif API_KEY:
        # Legacy fallback
        headers["X-API-Key"] = API_KEY

    return headers


async def call_api(tool: str, params: dict[str, Any]) -> dict[str, Any]:
    """Call the Snipara MCP API."""
    async with httpx.AsyncClient(timeout=60.0) as client:
        response = await client.post(
            f"{API_URL}/v1/{PROJECT_ID}/mcp",
            headers=get_headers(),
            json={"tool": tool, "params": params},
        )
        response.raise_for_status()
        return response.json()


@server.list_tools()
async def list_tools() -> list[Tool]:
    """List available Snipara tools."""
    return [
        Tool(
            name="rlm_context_query",
            description="""Query optimized context from your documentation.

Returns ranked, relevant sections that fit within your token budget.
This is the PRIMARY tool - use it for any documentation questions.

Examples:
- "How does authentication work?"
- "What are the API endpoints?"
- "Where is the database schema?"

Returns sections with relevance scores, file paths, and line numbers.""",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "Your question"},
                    "max_tokens": {"type": "integer", "default": 4000, "description": "Token budget"},
                    "search_mode": {"type": "string", "enum": ["keyword", "semantic", "hybrid"], "default": "hybrid"},
                },
                "required": ["query"],
            },
        ),
        Tool(
            name="rlm_ask",
            description="Query documentation (basic). Use rlm_context_query for better results.",
            inputSchema={
                "type": "object",
                "properties": {"question": {"type": "string", "description": "The question to ask"}},
                "required": ["question"],
            },
        ),
        Tool(
            name="rlm_search",
            description="Search documentation for a pattern (regex supported).",
            inputSchema={
                "type": "object",
                "properties": {
                    "pattern": {"type": "string", "description": "Regex pattern"},
                    "max_results": {"type": "integer", "default": 20},
                },
                "required": ["pattern"],
            },
        ),
        Tool(
            name="rlm_decompose",
            description="Break complex query into sub-queries with execution order.",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "Complex question to decompose"},
                    "max_depth": {"type": "integer", "default": 2},
                },
                "required": ["query"],
            },
        ),
        Tool(
            name="rlm_multi_query",
            description="Execute multiple queries in one call with shared token budget.",
            inputSchema={
                "type": "object",
                "properties": {
                    "queries": {
                        "type": "array",
                        "items": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]},
                    },
                    "max_tokens": {"type": "integer", "default": 8000},
                },
                "required": ["queries"],
            },
        ),
        Tool(
            name="rlm_inject",
            description="Set session context for subsequent queries.",
            inputSchema={
                "type": "object",
                "properties": {
                    "context": {"type": "string", "description": "Context to inject"},
                    "append": {"type": "boolean", "default": False},
                },
                "required": ["context"],
            },
        ),
        Tool(
            name="rlm_context",
            description="Show current session context.",
            inputSchema={"type": "object", "properties": {}, "required": []},
        ),
        Tool(
            name="rlm_clear_context",
            description="Clear session context.",
            inputSchema={"type": "object", "properties": {}, "required": []},
        ),
        Tool(
            name="rlm_stats",
            description="Show documentation statistics.",
            inputSchema={"type": "object", "properties": {}, "required": []},
        ),
        Tool(
            name="rlm_sections",
            description="List all document sections.",
            inputSchema={"type": "object", "properties": {}, "required": []},
        ),
        Tool(
            name="rlm_read",
            description="Read specific lines from documentation.",
            inputSchema={
                "type": "object",
                "properties": {"start_line": {"type": "integer"}, "end_line": {"type": "integer"}},
                "required": ["start_line", "end_line"],
            },
        ),
        Tool(
            name="rlm_settings",
            description="Show current project settings from dashboard (max_tokens, search_mode, etc.).",
            inputSchema={"type": "object", "properties": {"refresh": {"type": "boolean", "default": False, "description": "Force refresh from API"}}, "required": []},
        ),
        Tool(
            name="rlm_upload_document",
            description="Upload or update a document in the project. Supports .md, .txt, .mdx files.",
            inputSchema={
                "type": "object",
                "properties": {
                    "path": {"type": "string", "description": "Document path (e.g., 'docs/api.md')"},
                    "content": {"type": "string", "description": "Document content (markdown)"},
                },
                "required": ["path", "content"],
            },
        ),
        Tool(
            name="rlm_sync_documents",
            description="Bulk sync multiple documents. Use for batch uploads or CI/CD integration.",
            inputSchema={
                "type": "object",
                "properties": {
                    "documents": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "path": {"type": "string"},
                                "content": {"type": "string"},
                            },
                            "required": ["path", "content"],
                        },
                        "description": "Documents to sync",
                    },
                    "delete_missing": {"type": "boolean", "default": False, "description": "Delete docs not in list"},
                },
                "required": ["documents"],
            },
        ),
        # Phase 4.5: Planning
        Tool(
            name="rlm_plan",
            description="Generate execution plan for complex queries. Returns step-by-step plan with dependencies.",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "Complex question to plan for"},
                    "strategy": {
                        "type": "string",
                        "enum": ["breadth_first", "depth_first", "relevance_first"],
                        "default": "relevance_first",
                        "description": "Execution strategy",
                    },
                    "max_tokens": {"type": "integer", "default": 16000, "description": "Total token budget"},
                },
                "required": ["query"],
            },
        ),
        # Phase 4.6: Summary Storage
        Tool(
            name="rlm_store_summary",
            description="Store LLM-generated summary for a document. Enables faster future queries.",
            inputSchema={
                "type": "object",
                "properties": {
                    "document_path": {"type": "string", "description": "Path to the document"},
                    "summary": {"type": "string", "description": "Summary text to store"},
                    "summary_type": {
                        "type": "string",
                        "enum": ["concise", "detailed", "technical", "keywords", "custom"],
                        "default": "concise",
                        "description": "Type of summary",
                    },
                    "section_id": {"type": "string", "description": "Optional section identifier"},
                    "line_start": {"type": "integer", "description": "Optional start line"},
                    "line_end": {"type": "integer", "description": "Optional end line"},
                    "generated_by": {"type": "string", "description": "Model that generated the summary"},
                },
                "required": ["document_path", "summary"],
            },
        ),
        Tool(
            name="rlm_get_summaries",
            description="Retrieve stored summaries with optional filters.",
            inputSchema={
                "type": "object",
                "properties": {
                    "document_path": {"type": "string", "description": "Filter by document path"},
                    "summary_type": {
                        "type": "string",
                        "enum": ["concise", "detailed", "technical", "keywords", "custom"],
                        "description": "Filter by summary type",
                    },
                    "section_id": {"type": "string", "description": "Filter by section ID"},
                    "include_content": {"type": "boolean", "default": True, "description": "Include summary content"},
                },
                "required": [],
            },
        ),
        Tool(
            name="rlm_delete_summary",
            description="Delete stored summaries by ID, document path, or type.",
            inputSchema={
                "type": "object",
                "properties": {
                    "summary_id": {"type": "string", "description": "Specific summary ID to delete"},
                    "document_path": {"type": "string", "description": "Delete all summaries for document"},
                    "summary_type": {
                        "type": "string",
                        "enum": ["concise", "detailed", "technical", "keywords", "custom"],
                        "description": "Delete summaries of this type",
                    },
                },
                "required": [],
            },
        ),
        # Phase 7: Shared Context
        Tool(
            name="rlm_shared_context",
            description="Get merged context from linked shared collections. Returns categorized docs with budget allocation.",
            inputSchema={
                "type": "object",
                "properties": {
                    "max_tokens": {"type": "integer", "default": 4000, "description": "Token budget"},
                    "categories": {
                        "type": "array",
                        "items": {
                            "type": "string",
                            "enum": ["MANDATORY", "BEST_PRACTICES", "GUIDELINES", "REFERENCE"],
                        },
                        "description": "Filter by categories (default: all)",
                    },
                    "include_content": {"type": "boolean", "default": True, "description": "Include merged content"},
                },
                "required": [],
            },
        ),
        Tool(
            name="rlm_list_templates",
            description="List available prompt templates from shared collections.",
            inputSchema={
                "type": "object",
                "properties": {
                    "category": {"type": "string", "description": "Filter by category"},
                },
                "required": [],
            },
        ),
        Tool(
            name="rlm_get_template",
            description="Get a specific prompt template by ID or slug. Optionally render with variables.",
            inputSchema={
                "type": "object",
                "properties": {
                    "template_id": {"type": "string", "description": "Template ID"},
                    "slug": {"type": "string", "description": "Template slug"},
                    "variables": {
                        "type": "object",
                        "additionalProperties": {"type": "string"},
                        "description": "Variables to substitute in template",
                    },
                },
                "required": [],
            },
        ),
        # Phase 8.2: Agent Memory Tools
        Tool(
            name="rlm_remember",
            description="""Store a memory for later semantic recall.

Memories can be facts, decisions, learnings, preferences, todos, or context.
Each memory gets a confidence score that decays over time if not accessed.

Examples:
- Remember a user preference: type="preference", content="User prefers dark mode"
- Store a learned fact: type="fact", content="API uses JWT for auth"
- Record a decision: type="decision", content="Chose React over Vue for frontend"
""",
            inputSchema={
                "type": "object",
                "properties": {
                    "content": {"type": "string", "description": "The memory content to store"},
                    "type": {
                        "type": "string",
                        "enum": ["fact", "decision", "learning", "preference", "todo", "context"],
                        "default": "fact",
                        "description": "Type of memory",
                    },
                    "scope": {
                        "type": "string",
                        "enum": ["agent", "project", "team", "user"],
                        "default": "project",
                        "description": "Visibility scope",
                    },
                    "category": {"type": "string", "description": "Optional category for grouping"},
                    "ttl_days": {"type": "integer", "description": "Days until expiration (null = permanent)"},
                    "related_to": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "IDs of related memories",
                    },
                    "document_refs": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Referenced document paths",
                    },
                },
                "required": ["content"],
            },
        ),
        Tool(
            name="rlm_recall",
            description="""Semantically recall relevant memories based on a query.

Uses embeddings to find memories similar to the query, weighted by confidence.
Confidence decays over time if memories aren't accessed.

Examples:
- "What did the user say about their preferences?"
- "What decisions did we make about the architecture?"
- "What did I learn about the authentication system?"
""",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "Search query"},
                    "type": {
                        "type": "string",
                        "enum": ["fact", "decision", "learning", "preference", "todo", "context"],
                        "description": "Filter by memory type",
                    },
                    "scope": {
                        "type": "string",
                        "enum": ["agent", "project", "team", "user"],
                        "description": "Filter by scope",
                    },
                    "category": {"type": "string", "description": "Filter by category"},
                    "limit": {"type": "integer", "default": 5, "description": "Maximum memories to return"},
                    "min_relevance": {"type": "number", "default": 0.5, "description": "Minimum relevance score (0-1)"},
                },
                "required": ["query"],
            },
        ),
        Tool(
            name="rlm_memories",
            description="List memories with optional filters. For browsing stored memories without semantic search.",
            inputSchema={
                "type": "object",
                "properties": {
                    "type": {
                        "type": "string",
                        "enum": ["fact", "decision", "learning", "preference", "todo", "context"],
                        "description": "Filter by memory type",
                    },
                    "scope": {
                        "type": "string",
                        "enum": ["agent", "project", "team", "user"],
                        "description": "Filter by scope",
                    },
                    "category": {"type": "string", "description": "Filter by category"},
                    "search": {"type": "string", "description": "Text search in content"},
                    "limit": {"type": "integer", "default": 20, "description": "Maximum memories to return"},
                    "offset": {"type": "integer", "default": 0, "description": "Pagination offset"},
                },
                "required": [],
            },
        ),
        Tool(
            name="rlm_forget",
            description="Delete memories by ID or filter criteria. Use with caution.",
            inputSchema={
                "type": "object",
                "properties": {
                    "memory_id": {"type": "string", "description": "Specific memory ID to delete"},
                    "type": {
                        "type": "string",
                        "enum": ["fact", "decision", "learning", "preference", "todo", "context"],
                        "description": "Delete all of this type",
                    },
                    "category": {"type": "string", "description": "Delete all in this category"},
                    "older_than_days": {"type": "integer", "description": "Delete memories older than N days"},
                },
                "required": [],
            },
        ),
        # Phase 9.1: Multi-Agent Swarm Tools
        Tool(
            name="rlm_swarm_create",
            description="""Create a new agent swarm for multi-agent coordination.

Swarms allow multiple agents to work together with shared state, resource claims, and task queues.
""",
            inputSchema={
                "type": "object",
                "properties": {
                    "name": {"type": "string", "description": "Swarm name"},
                    "description": {"type": "string", "description": "Swarm description"},
                    "max_agents": {"type": "integer", "default": 10, "description": "Maximum agents allowed"},
                    "config": {"type": "object", "description": "Optional swarm configuration"},
                },
                "required": ["name"],
            },
        ),
        Tool(
            name="rlm_swarm_join",
            description="Join an existing swarm as an agent.",
            inputSchema={
                "type": "object",
                "properties": {
                    "swarm_id": {"type": "string", "description": "Swarm to join"},
                    "agent_id": {"type": "string", "description": "Your unique agent identifier"},
                    "role": {
                        "type": "string",
                        "enum": ["coordinator", "worker", "observer"],
                        "default": "worker",
                        "description": "Your role in the swarm",
                    },
                    "capabilities": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Your capabilities (e.g., 'code', 'test', 'review')",
                    },
                },
                "required": ["swarm_id", "agent_id"],
            },
        ),
        Tool(
            name="rlm_claim",
            description="""Claim exclusive access to a resource (file, function, module).

Claims prevent other agents from modifying the same resource simultaneously.
Claims auto-expire after timeout to prevent deadlocks.
""",
            inputSchema={
                "type": "object",
                "properties": {
                    "swarm_id": {"type": "string", "description": "Swarm ID"},
                    "agent_id": {"type": "string", "description": "Your agent identifier"},
                    "resource_type": {
                        "type": "string",
                        "enum": ["file", "function", "module", "component", "other"],
                        "description": "Type of resource",
                    },
                    "resource_id": {"type": "string", "description": "Resource identifier (e.g., file path)"},
                    "timeout_seconds": {"type": "integer", "default": 300, "description": "Claim timeout"},
                },
                "required": ["swarm_id", "agent_id", "resource_type", "resource_id"],
            },
        ),
        Tool(
            name="rlm_release",
            description="Release a claimed resource.",
            inputSchema={
                "type": "object",
                "properties": {
                    "swarm_id": {"type": "string", "description": "Swarm ID"},
                    "agent_id": {"type": "string", "description": "Your agent identifier"},
                    "claim_id": {"type": "string", "description": "Claim ID to release"},
                    "resource_type": {"type": "string", "description": "Resource type (alternative to claim_id)"},
                    "resource_id": {"type": "string", "description": "Resource ID (alternative to claim_id)"},
                },
                "required": ["swarm_id", "agent_id"],
            },
        ),
        Tool(
            name="rlm_state_get",
            description="Read shared swarm state by key.",
            inputSchema={
                "type": "object",
                "properties": {
                    "swarm_id": {"type": "string", "description": "Swarm ID"},
                    "key": {"type": "string", "description": "State key to read"},
                },
                "required": ["swarm_id", "key"],
            },
        ),
        Tool(
            name="rlm_state_set",
            description="""Write shared swarm state with optimistic locking.

Provide expected_version to prevent lost updates from concurrent modifications.
""",
            inputSchema={
                "type": "object",
                "properties": {
                    "swarm_id": {"type": "string", "description": "Swarm ID"},
                    "agent_id": {"type": "string", "description": "Your agent identifier"},
                    "key": {"type": "string", "description": "State key"},
                    "value": {"description": "Value to set (any JSON-serializable type)"},
                    "expected_version": {"type": "integer", "description": "Expected version for optimistic locking"},
                },
                "required": ["swarm_id", "agent_id", "key", "value"],
            },
        ),
        Tool(
            name="rlm_broadcast",
            description="Send an event to all agents in the swarm.",
            inputSchema={
                "type": "object",
                "properties": {
                    "swarm_id": {"type": "string", "description": "Swarm ID"},
                    "agent_id": {"type": "string", "description": "Your agent identifier"},
                    "event_type": {"type": "string", "description": "Event type (e.g., 'task_completed', 'error')"},
                    "payload": {"type": "object", "description": "Event data"},
                },
                "required": ["swarm_id", "agent_id", "event_type"],
            },
        ),
        Tool(
            name="rlm_task_create",
            description="Create a task in the swarm's distributed task queue.",
            inputSchema={
                "type": "object",
                "properties": {
                    "swarm_id": {"type": "string", "description": "Swarm ID"},
                    "agent_id": {"type": "string", "description": "Creating agent identifier"},
                    "title": {"type": "string", "description": "Task title"},
                    "description": {"type": "string", "description": "Task description"},
                    "priority": {"type": "integer", "default": 0, "description": "Priority (higher = more urgent)"},
                    "depends_on": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Task IDs this depends on",
                    },
                    "metadata": {"type": "object", "description": "Additional task data"},
                },
                "required": ["swarm_id", "agent_id", "title"],
            },
        ),
        Tool(
            name="rlm_task_claim",
            description="""Claim a task from the queue.

If task_id not specified, claims the highest priority task with all dependencies satisfied.
""",
            inputSchema={
                "type": "object",
                "properties": {
                    "swarm_id": {"type": "string", "description": "Swarm ID"},
                    "agent_id": {"type": "string", "description": "Your agent identifier"},
                    "task_id": {"type": "string", "description": "Specific task to claim (optional)"},
                    "timeout_seconds": {"type": "integer", "default": 600, "description": "Task timeout"},
                },
                "required": ["swarm_id", "agent_id"],
            },
        ),
        Tool(
            name="rlm_task_complete",
            description="Mark a claimed task as completed or failed.",
            inputSchema={
                "type": "object",
                "properties": {
                    "swarm_id": {"type": "string", "description": "Swarm ID"},
                    "agent_id": {"type": "string", "description": "Your agent identifier"},
                    "task_id": {"type": "string", "description": "Task to complete"},
                    "success": {"type": "boolean", "default": True, "description": "Whether task succeeded"},
                    "result": {"description": "Task result data"},
                },
                "required": ["swarm_id", "agent_id", "task_id"],
            },
        ),
    ]


@server.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
    """Handle tool calls."""
    global _session_context

    try:
        if name == "rlm_context_query":
            # Get project settings from dashboard (cached)
            settings = await get_project_settings()

            query = arguments["query"]
            if _session_context:
                query = f"Context: {_session_context}\n\nQuestion: {query}"

            # Use settings from dashboard, allow override from arguments
            max_tokens = arguments.get("max_tokens") or settings.get("maxTokensPerQuery", 4000)
            search_mode = arguments.get("search_mode") or settings.get("searchMode", "hybrid")
            include_summaries = settings.get("includeSummaries", True)

            result = await call_api("rlm_context_query", {
                "query": query,
                "max_tokens": max_tokens,
                "search_mode": search_mode,
                "include_metadata": True,
                "prefer_summaries": include_summaries,
            })

            if result.get("success"):
                data = result.get("result", {})
                sections = data.get("sections", [])
                if sections:
                    parts = []
                    # Prepend system instructions if provided
                    if data.get("system_instructions"):
                        parts.append(data["system_instructions"])
                    parts.append("## Relevant Documentation\n")
                    for s in sections:
                        parts.append(f"### {s.get('title', 'Untitled')}")
                        parts.append(f"*{s.get('file', '')} | Score: {s.get('relevance_score', 0):.2f}*\n")
                        parts.append(s.get("content", ""))
                        parts.append("")
                    parts.append(f"---\n*{len(sections)} sections, {data.get('total_tokens', 0)} tokens*")
                    return [TextContent(type="text", text="\n".join(parts))]
                return [TextContent(type="text", text="No relevant documentation found.")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_ask":
            question = arguments["question"]
            if _session_context:
                question = f"Context: {_session_context}\n\nQuestion: {question}"

            result = await call_api("rlm_context_query", {
                "query": question, "max_tokens": 4000, "search_mode": "hybrid", "include_metadata": True,
            })

            if result.get("success"):
                data = result.get("result", {})
                sections = data.get("sections", [])
                if sections:
                    parts = ["## Relevant Documentation\n"]
                    for s in sections:
                        parts.append(f"### {s.get('title', 'Untitled')}")
                        parts.append(f"*{s.get('file', '')}*\n")
                        parts.append(s.get("content", ""))
                        parts.append("")
                    return [TextContent(type="text", text="\n".join(parts))]
                return [TextContent(type="text", text="No relevant documentation found.")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_search":
            result = await call_api("rlm_search", {"pattern": arguments["pattern"]})
            if result.get("success"):
                matches = result.get("result", {}).get("matches", [])
                max_results = arguments.get("max_results", 20)
                if matches:
                    lines = [f"Found {len(matches)} matches:\n"]
                    for m in matches[:max_results]:
                        lines.append(f"  {m.get('file', '')}:{m.get('line_number', 0)}: {m.get('content', '')[:100]}")
                    if len(matches) > max_results:
                        lines.append(f"\n... and {len(matches) - max_results} more")
                    return [TextContent(type="text", text="\n".join(lines))]
                return [TextContent(type="text", text=f"No matches for '{arguments['pattern']}'")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_decompose":
            result = await call_api("rlm_decompose", {"query": arguments["query"], "max_depth": arguments.get("max_depth", 2)})
            if result.get("success"):
                data = result.get("result", {})
                sub = data.get("sub_queries", [])
                lines = [f"**Decomposed into {len(sub)} sub-queries:**\n"]
                for q in sub:
                    lines.append(f"{q.get('id', 0)}. {q.get('query', '')} (priority: {q.get('priority', 1)})")
                lines.append(f"\n**Suggested order:** {data.get('suggested_sequence', [])}")
                return [TextContent(type="text", text="\n".join(lines))]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_multi_query":
            result = await call_api("rlm_multi_query", {
                "queries": arguments["queries"], "max_tokens": arguments.get("max_tokens", 8000),
            })
            if result.get("success"):
                data = result.get("result", {})
                parts = [f"**Executed {data.get('queries_executed', 0)} queries:**\n"]
                for r in data.get("results", []):
                    parts.append(f"### {r.get('query', '')}")
                    for s in r.get("sections", [])[:2]:
                        parts.append(f"- {s.get('title', '')} ({s.get('file', '')})")
                    parts.append("")
                parts.append(f"*Total: {data.get('total_tokens', 0)} tokens*")
                return [TextContent(type="text", text="\n".join(parts))]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_inject":
            ctx = arguments["context"]
            if arguments.get("append") and _session_context:
                _session_context = _session_context + "\n\n" + ctx
            else:
                _session_context = ctx
            try:
                await call_api("rlm_inject", {"context": _session_context})
            except Exception:
                pass
            return [TextContent(type="text", text=f"Session context {'appended' if arguments.get('append') else 'set'} ({len(_session_context)} chars)")]

        elif name == "rlm_context":
            if _session_context:
                return [TextContent(type="text", text=f"**Session Context:**\n\n{_session_context}")]
            return [TextContent(type="text", text="No session context. Use rlm_inject to set.")]

        elif name == "rlm_clear_context":
            if _session_context:
                _session_context = ""
                try:
                    await call_api("rlm_clear_context", {})
                except Exception:
                    pass
                return [TextContent(type="text", text="Session context cleared.")]
            return [TextContent(type="text", text="No context to clear.")]

        elif name == "rlm_stats":
            result = await call_api("rlm_stats", {})
            if result.get("success"):
                # Handle both "result" and "data" keys from different API responses
                d = result.get("result", result.get("data", {}))
                # Backend returns total_characters, not total_tokens
                files_loaded = d.get('files_loaded', d.get('document_count', 0))
                total_lines = d.get('total_lines', 0)
                total_chars = d.get('total_characters', 0)
                sections = d.get('sections', d.get('chunk_count', 0))
                # Safe formatting - convert to int if numeric string, else show as-is
                def fmt(v):
                    if isinstance(v, (int, float)):
                        return f"{int(v):,}"
                    if isinstance(v, str) and v.isdigit():
                        return f"{int(v):,}"
                    return str(v) if v else "0"
                return [TextContent(type="text", text=f"**Stats:**\n- Files: {fmt(files_loaded)}\n- Lines: {fmt(total_lines)}\n- Characters: {fmt(total_chars)}\n- Sections: {fmt(sections)}")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_sections":
            result = await call_api("rlm_sections", {})
            if result.get("success"):
                sections = result.get("result", {}).get("sections", [])
                lines = ["**Documents:**\n"]
                for s in sections:
                    lines.append(f"- {s.get('path', '')} ({s.get('chunk_count', 0)} chunks)")
                return [TextContent(type="text", text="\n".join(lines))]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_read":
            result = await call_api("rlm_read", {"start_line": arguments["start_line"], "end_line": arguments["end_line"]})
            if result.get("success"):
                content = result.get("result", {}).get("content", "")
                return [TextContent(type="text", text=f"**Lines {arguments['start_line']}-{arguments['end_line']}:**\n```\n{content}\n```")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_settings":
            global _settings_cache, _settings_cache_time
            # Force refresh if requested
            if arguments.get("refresh"):
                _settings_cache = {}
                _settings_cache_time = 0
            settings = await get_project_settings()
            cache_age = int(time.time() - _settings_cache_time) if _settings_cache_time else 0
            lines = [
                "**Project Settings** (from dashboard)\n",
                f"- Max Tokens: {settings.get('maxTokensPerQuery', 4000)}",
                f"- Search Mode: {settings.get('searchMode', 'hybrid')}",
                f"- Include Summaries: {settings.get('includeSummaries', True)}",
                f"- Auto-Inject Context: {settings.get('autoInjectContext', False)}",
                f"- Enrich Prompts: {settings.get('enrichPrompts', False)}",
                f"\n*Cache age: {cache_age}s (TTL: {SETTINGS_CACHE_TTL}s)*",
            ]
            return [TextContent(type="text", text="\n".join(lines))]

        elif name == "rlm_upload_document":
            result = await call_api("rlm_upload_document", {
                "path": arguments["path"],
                "content": arguments["content"],
            })
            if result.get("success"):
                data = result.get("result", {})
                return [TextContent(type="text", text=f"**Document {data.get('action', 'processed')}:** {data.get('path', '')} ({data.get('size', 0)} bytes)")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_sync_documents":
            result = await call_api("rlm_sync_documents", {
                "documents": arguments["documents"],
                "delete_missing": arguments.get("delete_missing", False),
            })
            if result.get("success"):
                data = result.get("result", {})
                return [TextContent(type="text", text=f"**Sync complete:** {data.get('created', 0)} created, {data.get('updated', 0)} updated, {data.get('unchanged', 0)} unchanged, {data.get('deleted', 0)} deleted")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        # Phase 4.5: Planning
        elif name == "rlm_plan":
            result = await call_api("rlm_plan", {
                "query": arguments["query"],
                "strategy": arguments.get("strategy", "relevance_first"),
                "max_tokens": arguments.get("max_tokens", 16000),
            })
            if result.get("success"):
                data = result.get("result", {})
                steps = data.get("steps", [])
                lines = [
                    f"**Execution Plan** ({data.get('plan_id', 'unknown')})\n",
                    f"Strategy: {data.get('strategy', 'relevance_first')}",
                    f"Estimated tokens: {data.get('estimated_total_tokens', 0)}\n",
                    "**Steps:**",
                ]
                for step in steps:
                    deps = f" (depends on: {step.get('depends_on', [])})" if step.get("depends_on") else ""
                    lines.append(f"{step.get('step', 0)}. {step.get('action', '')} → {step.get('expected_output', '')}{deps}")
                return [TextContent(type="text", text="\n".join(lines))]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        # Phase 4.6: Summary Storage
        elif name == "rlm_store_summary":
            result = await call_api("rlm_store_summary", {
                "document_path": arguments["document_path"],
                "summary": arguments["summary"],
                "summary_type": arguments.get("summary_type", "concise"),
                "section_id": arguments.get("section_id"),
                "line_start": arguments.get("line_start"),
                "line_end": arguments.get("line_end"),
                "generated_by": arguments.get("generated_by"),
            })
            if result.get("success"):
                data = result.get("result", {})
                action = "created" if data.get("created") else "updated"
                return [TextContent(type="text", text=f"**Summary {action}:** {data.get('document_path', '')} ({data.get('summary_type', '')})\nTokens: {data.get('token_count', 0)} | ID: {data.get('summary_id', '')}")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_get_summaries":
            result = await call_api("rlm_get_summaries", {
                "document_path": arguments.get("document_path"),
                "summary_type": arguments.get("summary_type"),
                "section_id": arguments.get("section_id"),
                "include_content": arguments.get("include_content", True),
            })
            if result.get("success"):
                data = result.get("result", {})
                summaries = data.get("summaries", [])
                if summaries:
                    lines = [f"**Found {len(summaries)} summaries** ({data.get('total_tokens', 0)} tokens)\n"]
                    for s in summaries:
                        lines.append(f"- **{s.get('document_path', '')}** ({s.get('summary_type', '')})")
                        if s.get("content"):
                            preview = s["content"][:200] + "..." if len(s.get("content", "")) > 200 else s.get("content", "")
                            lines.append(f"  {preview}")
                    return [TextContent(type="text", text="\n".join(lines))]
                return [TextContent(type="text", text="No summaries found.")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_delete_summary":
            result = await call_api("rlm_delete_summary", {
                "summary_id": arguments.get("summary_id"),
                "document_path": arguments.get("document_path"),
                "summary_type": arguments.get("summary_type"),
            })
            if result.get("success"):
                data = result.get("result", {})
                return [TextContent(type="text", text=f"**Deleted:** {data.get('deleted_count', 0)} summary(ies)")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        # Phase 7: Shared Context
        elif name == "rlm_shared_context":
            result = await call_api("rlm_shared_context", {
                "max_tokens": arguments.get("max_tokens", 4000),
                "categories": arguments.get("categories"),
                "include_content": arguments.get("include_content", True),
            })
            if result.get("success"):
                data = result.get("result", {})
                docs = data.get("documents", [])
                if docs:
                    lines = [
                        f"**Shared Context** ({data.get('collections_loaded', 0)} collections)\n",
                        f"Total tokens: {data.get('total_tokens', 0)} | Hash: {data.get('context_hash', '')[:8]}...\n",
                    ]
                    # Group by category
                    by_cat: dict[str, list] = {}
                    for d in docs:
                        cat = d.get("category", "OTHER")
                        if cat not in by_cat:
                            by_cat[cat] = []
                        by_cat[cat].append(d)
                    for cat in ["MANDATORY", "BEST_PRACTICES", "GUIDELINES", "REFERENCE"]:
                        if cat in by_cat:
                            lines.append(f"**{cat}:**")
                            for d in by_cat[cat]:
                                lines.append(f"  - {d.get('title', 'Untitled')} ({d.get('token_count', 0)} tokens)")
                    if data.get("merged_content"):
                        lines.append("\n---\n")
                        lines.append(data["merged_content"])
                    return [TextContent(type="text", text="\n".join(lines))]
                return [TextContent(type="text", text="No shared context linked to this project.")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_list_templates":
            result = await call_api("rlm_list_templates", {
                "category": arguments.get("category"),
            })
            if result.get("success"):
                data = result.get("result", {})
                templates = data.get("templates", [])
                if templates:
                    lines = [f"**Available Templates** ({len(templates)} total)\n"]
                    # Group by category
                    by_cat: dict[str, list] = {}
                    for t in templates:
                        cat = t.get("category", "general")
                        if cat not in by_cat:
                            by_cat[cat] = []
                        by_cat[cat].append(t)
                    for cat, temps in by_cat.items():
                        lines.append(f"**{cat}:**")
                        for t in temps:
                            desc = f" - {t['description']}" if t.get("description") else ""
                            vars_str = f" (vars: {', '.join(t.get('variables', []))})" if t.get("variables") else ""
                            lines.append(f"  - `{t.get('slug', '')}`: {t.get('name', '')}{desc}{vars_str}")
                    return [TextContent(type="text", text="\n".join(lines))]
                return [TextContent(type="text", text="No templates found.")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_get_template":
            result = await call_api("rlm_get_template", {
                "template_id": arguments.get("template_id"),
                "slug": arguments.get("slug"),
                "variables": arguments.get("variables", {}),
            })
            if result.get("success"):
                data = result.get("result", {})
                template = data.get("template")
                if template:
                    lines = [
                        f"**{template.get('name', 'Template')}** (`{template.get('slug', '')}`)\n",
                        f"Category: {template.get('category', '')} | Collection: {template.get('collection_name', '')}",
                    ]
                    if template.get("description"):
                        lines.append(f"Description: {template['description']}")
                    if template.get("variables"):
                        lines.append(f"Variables: {', '.join(template['variables'])}")
                    missing = data.get("missing_variables", [])
                    if missing:
                        lines.append(f"\n⚠️ Missing variables: {', '.join(missing)}")
                    lines.append("\n**Rendered Prompt:**\n```")
                    lines.append(data.get("rendered_prompt", template.get("prompt", "")))
                    lines.append("```")
                    return [TextContent(type="text", text="\n".join(lines))]
                return [TextContent(type="text", text="Template not found.")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        # Phase 8.2: Agent Memory Handlers
        elif name == "rlm_remember":
            result = await call_api("rlm_remember", {
                "content": arguments["content"],
                "type": arguments.get("type", "fact"),
                "scope": arguments.get("scope", "project"),
                "category": arguments.get("category"),
                "ttl_days": arguments.get("ttl_days"),
                "related_to": arguments.get("related_to"),
                "document_refs": arguments.get("document_refs"),
            })
            if result.get("success"):
                data = result.get("result", {})
                return [TextContent(type="text", text=f"**Memory stored** (ID: {data.get('memory_id', '')})\nType: {data.get('type', '')} | Scope: {data.get('scope', '')}")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_recall":
            result = await call_api("rlm_recall", {
                "query": arguments["query"],
                "type": arguments.get("type"),
                "scope": arguments.get("scope"),
                "category": arguments.get("category"),
                "limit": arguments.get("limit", 5),
                "min_relevance": arguments.get("min_relevance", 0.5),
            })
            if result.get("success"):
                data = result.get("result", {})
                memories = data.get("memories", [])
                if memories:
                    lines = [f"**Recalled {len(memories)} memories** (searched {data.get('total_searched', 0)})\n"]
                    for m in memories:
                        conf = m.get("confidence", 1.0)
                        rel = m.get("relevance", 0)
                        lines.append(f"**[{m.get('type', '')}]** {m.get('content', '')[:200]}")
                        lines.append(f"  *Relevance: {rel:.2f} | Confidence: {conf:.2f} | Accessed: {m.get('access_count', 0)}x*\n")
                    return [TextContent(type="text", text="\n".join(lines))]
                return [TextContent(type="text", text="No relevant memories found.")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_memories":
            result = await call_api("rlm_memories", {
                "type": arguments.get("type"),
                "scope": arguments.get("scope"),
                "category": arguments.get("category"),
                "search": arguments.get("search"),
                "limit": arguments.get("limit", 20),
                "offset": arguments.get("offset", 0),
            })
            if result.get("success"):
                data = result.get("result", {})
                memories = data.get("memories", [])
                if memories:
                    lines = [f"**{data.get('total_count', len(memories))} memories** (showing {len(memories)})\n"]
                    for m in memories:
                        lines.append(f"- **[{m.get('type', '')}]** {m.get('content', '')[:100]}...")
                        lines.append(f"  *ID: {m.get('memory_id', '')} | Category: {m.get('category', 'none')}*")
                    if data.get("has_more"):
                        lines.append(f"\n*More results available (offset: {arguments.get('offset', 0) + len(memories)})*")
                    return [TextContent(type="text", text="\n".join(lines))]
                return [TextContent(type="text", text="No memories found.")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_forget":
            result = await call_api("rlm_forget", {
                "memory_id": arguments.get("memory_id"),
                "type": arguments.get("type"),
                "category": arguments.get("category"),
                "older_than_days": arguments.get("older_than_days"),
            })
            if result.get("success"):
                data = result.get("result", {})
                return [TextContent(type="text", text=f"**{data.get('message', 'Deleted')}** ({data.get('deleted_count', 0)} memories)")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        # Phase 9.1: Multi-Agent Swarm Handlers
        elif name == "rlm_swarm_create":
            result = await call_api("rlm_swarm_create", {
                "name": arguments["name"],
                "description": arguments.get("description"),
                "max_agents": arguments.get("max_agents", 10),
                "config": arguments.get("config"),
            })
            if result.get("success"):
                data = result.get("result", {})
                return [TextContent(type="text", text=f"**Swarm created:** {data.get('name', '')}\nID: {data.get('swarm_id', '')} | Max agents: {data.get('max_agents', 10)}")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_swarm_join":
            result = await call_api("rlm_swarm_join", {
                "swarm_id": arguments["swarm_id"],
                "agent_id": arguments["agent_id"],
                "role": arguments.get("role", "worker"),
                "capabilities": arguments.get("capabilities"),
            })
            if result.get("success"):
                data = result.get("result", {})
                return [TextContent(type="text", text=f"**Joined swarm** as {data.get('role', 'worker')}\nAgent ID: {data.get('agent_id', '')}")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_claim":
            result = await call_api("rlm_claim", {
                "swarm_id": arguments["swarm_id"],
                "agent_id": arguments["agent_id"],
                "resource_type": arguments["resource_type"],
                "resource_id": arguments["resource_id"],
                "timeout_seconds": arguments.get("timeout_seconds", 300),
            })
            if result.get("success"):
                data = result.get("result", {})
                if data.get("extended"):
                    return [TextContent(type="text", text=f"**Claim extended** until {data.get('expires_at', '')}")]
                return [TextContent(type="text", text=f"**Resource claimed:** {data.get('resource_type', '')}:{data.get('resource_id', '')}\nExpires: {data.get('expires_at', '')}")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_release":
            result = await call_api("rlm_release", {
                "swarm_id": arguments["swarm_id"],
                "agent_id": arguments["agent_id"],
                "claim_id": arguments.get("claim_id"),
                "resource_type": arguments.get("resource_type"),
                "resource_id": arguments.get("resource_id"),
            })
            if result.get("success"):
                return [TextContent(type="text", text="**Claim released**")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_state_get":
            result = await call_api("rlm_state_get", {
                "swarm_id": arguments["swarm_id"],
                "key": arguments["key"],
            })
            if result.get("success"):
                data = result.get("result", {})
                if data.get("found"):
                    import json as json_mod
                    value_str = json_mod.dumps(data.get("value"), indent=2) if isinstance(data.get("value"), (dict, list)) else str(data.get("value"))
                    return [TextContent(type="text", text=f"**{arguments['key']}** (v{data.get('version', 0)})\n```json\n{value_str}\n```")]
                return [TextContent(type="text", text=f"Key '{arguments['key']}' not found")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_state_set":
            result = await call_api("rlm_state_set", {
                "swarm_id": arguments["swarm_id"],
                "agent_id": arguments["agent_id"],
                "key": arguments["key"],
                "value": arguments["value"],
                "expected_version": arguments.get("expected_version"),
            })
            if result.get("success"):
                data = result.get("result", {})
                return [TextContent(type="text", text=f"**State {data.get('message', 'updated')}:** {arguments['key']} → v{data.get('version', 0)}")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_broadcast":
            result = await call_api("rlm_broadcast", {
                "swarm_id": arguments["swarm_id"],
                "agent_id": arguments["agent_id"],
                "event_type": arguments["event_type"],
                "payload": arguments.get("payload"),
            })
            if result.get("success"):
                data = result.get("result", {})
                redis_status = "✓ real-time" if data.get("redis_published") else "✗ persisted only"
                return [TextContent(type="text", text=f"**Event broadcast:** {arguments['event_type']} ({redis_status})")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_task_create":
            result = await call_api("rlm_task_create", {
                "swarm_id": arguments["swarm_id"],
                "agent_id": arguments["agent_id"],
                "title": arguments["title"],
                "description": arguments.get("description"),
                "priority": arguments.get("priority", 0),
                "depends_on": arguments.get("depends_on"),
                "metadata": arguments.get("metadata"),
            })
            if result.get("success"):
                data = result.get("result", {})
                deps = f" (depends on: {data.get('depends_on', [])})" if data.get("depends_on") else ""
                return [TextContent(type="text", text=f"**Task created:** {arguments['title']}\nID: {data.get('task_id', '')} | Priority: {data.get('priority', 0)}{deps}")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_task_claim":
            result = await call_api("rlm_task_claim", {
                "swarm_id": arguments["swarm_id"],
                "agent_id": arguments["agent_id"],
                "task_id": arguments.get("task_id"),
                "timeout_seconds": arguments.get("timeout_seconds", 600),
            })
            if result.get("success"):
                data = result.get("result", {})
                return [TextContent(type="text", text=f"**Task claimed:** {data.get('title', '')}\nID: {data.get('task_id', '')} | Deadline: {data.get('deadline', '')}")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        elif name == "rlm_task_complete":
            result = await call_api("rlm_task_complete", {
                "swarm_id": arguments["swarm_id"],
                "agent_id": arguments["agent_id"],
                "task_id": arguments["task_id"],
                "success": arguments.get("success", True),
                "result": arguments.get("result"),
            })
            if result.get("success"):
                data = result.get("result", {})
                return [TextContent(type="text", text=f"**Task {data.get('status', 'completed')}:** {arguments['task_id']}")]
            return [TextContent(type="text", text=f"**Error:** {result.get('error', 'Unknown error')}")]

        else:
            return [TextContent(type="text", text=f"Unknown tool: {name}")]

    except httpx.HTTPStatusError as e:
        return [TextContent(type="text", text=f"**API Error:** {e.response.status_code} - {e.response.text}")]
    except httpx.ConnectError:
        return [TextContent(type="text", text=f"**Connection Error:** Cannot reach {API_URL}")]
    except Exception as e:
        return [TextContent(type="text", text=f"**Error:** {type(e).__name__}: {str(e)}")]


async def run_server():
    """Run the MCP server."""
    global _auth_token, _auth_type, _project_id, API_KEY, PROJECT_ID

    # Reload auth in case it changed
    _auth_token, _auth_type, _project_id = _load_auth()
    [REDACTED_SECRET] or os.environ.get("SNIPARA_API_KEY", "")
    PROJECT_ID = _project_id or os.environ.get("SNIPARA_PROJECT_ID", "")

    if not _auth_token and not [REDACTED_SECRET]"Error: No authentication found.", file=sys.stderr)
        print("", file=sys.stderr)
        print("Options:", file=sys.stderr)
        print("  1. Run 'snipara-mcp-login' to authenticate via browser (recommended)", file=sys.stderr)
        print("  2. Set SNIPARA_API_KEY environment variable", file=sys.stderr)
        sys.exit(1)

    if not PROJECT_ID:
        print("Error: No project ID found.", file=sys.stderr)
        if _auth_type == "oauth":
            print("Your OAuth token should include a project ID. Try logging in again.", file=sys.stderr)
        else:
            print("Set SNIPARA_PROJECT_ID environment variable.", file=sys.stderr)
        sys.exit(1)

    # Log auth method for debugging
    if _auth_type == "oauth":
        print(f"Authenticated via OAuth (project: {PROJECT_ID})", file=sys.stderr)
    else:
        print(f"Authenticated via API key (project: {PROJECT_ID})", file=sys.stderr)

    async with stdio_server() as (read_stream, write_stream):
        await server.run(read_stream, write_stream, server.create_initialization_options())


def main():
    """Entry point for snipara-mcp command."""
    asyncio.run(run_server())


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)