Metadata-Version: 2.4
Name: odoo-mcp-gateway
Version: 0.2.2
Summary: Security-first, version-agnostic MCP gateway for Odoo 17/18/19 — works with stock and custom modules via YAML configuration
Project-URL: Homepage, https://github.com/parth-unjiya/odoo-mcp-gateway
Project-URL: Repository, https://github.com/parth-unjiya/odoo-mcp-gateway
Project-URL: Issues, https://github.com/parth-unjiya/odoo-mcp-gateway/issues
Author-email: Parth Unjiya <parth.unjiya@spaceo.in>
License-Expression: MIT
License-File: LICENSE
Keywords: ai,erp,gateway,mcp,odoo
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.10
Requires-Dist: defusedxml>=0.7.1
Requires-Dist: httpx>=0.25.0
Requires-Dist: mcp>=1.9.4
Requires-Dist: pydantic-settings>=2.0
Requires-Dist: pydantic>=2.0
Requires-Dist: python-dotenv>=1.0
Requires-Dist: pyyaml>=6.0
Provides-Extra: dev
Requires-Dist: mypy>=1.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Requires-Dist: types-defusedxml>=0.7; extra == 'dev'
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
Description-Content-Type: text/markdown

# odoo-mcp-gateway

Security-first, version-agnostic MCP gateway for Odoo 17/18/19. Works with stock and custom modules via YAML configuration. Zero Odoo-side code required.

[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
[![Odoo](https://img.shields.io/badge/Odoo-17%20%7C%2018%20%7C%2019-714B67.svg)](https://www.odoo.com/)
[![Tests](https://img.shields.io/badge/tests-1476%20passing-brightgreen.svg)](#testing)
[![Coverage](https://img.shields.io/badge/coverage-93%25-brightgreen.svg)](#testing)

<!-- mcp-name: io.github.parth-unjiya/odoo-mcp-gateway -->

## 30-Second Quick Start

```bash
pip install odoo-mcp-gateway
```

Add to Claude Desktop config (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "odoo": {
      "command": "python",
      "args": ["-m", "odoo_mcp_gateway"],
      "env": {
        "ODOO_URL": "http://localhost:8069",
        "ODOO_DB": "your_database"
      }
    }
  }
}
```

Restart Claude Desktop. In any conversation:

```
login with method "password", username "admin", credential "your_password"
```

You're connected. Ask Claude to query, create, or update Odoo records — every call is rate-limited, audit-logged, and runs through two layers of security checks before reaching Odoo.

**No Odoo addon required. No Python code to write.** Just YAML config for fine-grained access control (optional — secure defaults work out of the box).

## What's New in v0.2.1

- **Brute-force protection** — per-username (5/5min) + per-source (30/15min) lockout
- **`dry_run` mode** on `create_record`, `update_record`, `delete_record`, `execute_method` — validate without executing
- **2 new tools**: `get_defaults` (preview Odoo defaults), `get_onchange` (preview field side effects)
- **Temporal grouping** in `read_group`: `create_date:month`, `date:quarter`, etc.
- **Hardened blocklists**: 32 always-blocked models (was 17), 29 always-blocked methods (was 18), 10 always-blocked write fields, 8 always read-only models
- **Server-side admin verification** via `has_group('base.group_system')` (was trusted from auth response)
- **Credential wrapper** prevents password leakage via `repr()`/traceback
- **JSON-RPC retry** only fires on `OdooSessionExpiredError` (was retrying on every auth error)

See [CHANGELOG.md](CHANGELOG.md) for the full list of 21 security fixes.

## Why This Exists

Existing Odoo MCP servers share common problems: hardcoded model lists that miss custom modules, security as an afterthought, mandatory custom Odoo addons, and single-version targets. This gateway solves all of them:

- **Two-layer security** — MCP restrictions (YAML) + Odoo's built-in ACLs (ir.model.access + ir.rule)
- **YAML-driven configuration** — model restrictions, RBAC, field-level access, rate limiting, audit logging
- **Custom module support** — auto-discovers models via `ir.model`, add YAML config and it works
- **Version-agnostic** — Odoo 17, 18, 19 with version-specific adapters
- **Zero Odoo-side code** — `pip install` + YAML config = done. No custom addon required
- **Full MCP primitives** — 31 Tools + 6 Resources + 12 Prompts (most servers only implement Tools)
- **Plugin architecture** — extend with pip-installable domain packs via entry_points

## Architecture

```
MCP Client (Claude Desktop / Claude Code / HTTP)
    |  User calls login tool with Odoo credentials
    v
MCP Server (FastMCP)
    |
    |-- security_gate()    --> Rate limit + RBAC tool access + audit logging
    |-- restrictions       --> Model/method/field block lists (YAML + hardcoded)
    |-- rbac               --> Field-level filtering + write sanitization
    |
    |-- tools/             --> 31 MCP tools (auth + schema + CRUD + workflow + plugins)
    |-- resources/         --> 6 MCP resources (odoo:// URIs)
    |-- prompts/           --> 12 reusable prompt templates
    |-- plugins/           --> Entry-point plugin system (HR, Sales, Project, Helpdesk)
    |
    |  JSON-RPC / XML-RPC as authenticated user
    v
Odoo 17/18/19 (security enforced per user via ir.model.access + ir.rule)
```

### Security Pipeline

Every tool and resource call passes through this pipeline:

```
Request --> Rate Limit --> Authentication Check --> RBAC Tool Access
    --> Model Restriction --> Method Restriction --> Field Validation
    --> Handler Execution --> RBAC Field Filtering --> Audit Log --> Response
```

Hardcoded safety guardrails that cannot be overridden by YAML:
- **32 always-blocked models** — system internals, auth/TOTP, payment tokens, attachments, mail.mail, base.automation, and more
- **8 always read-only models** — mail.message, mail.followers, mail.activity, discuss.channel, mail.notification, mail.compose.message, mail.alias, discuss.channel.member (reads OK, writes blocked for everyone)
- **10 always-blocked write fields** — password, password_crypt, groups_id, totp_secret, signup_token/type/expiration, api_key, share, active
- **29 always-blocked methods** — sudo, with_user/env/context, _sql, _write, _create, name_create, load, import_data, export_data, and more
- **28 ORM methods blocked in execute_method** (prevents bypassing field-level checks)
- **Per-username brute-force lockout** — 5 failures → 5 minute lockout (fixed duration, cannot be extended)
- **Per-source brute-force lockout** — 30 failures / 15 min, prevents username-rotation attacks
- **Credential wrapper class** — passwords stored with leak-safe `__repr__`/`__str__`, cleared on close
- **Server-side admin verification** — `has_group('base.group_system')` overrides auth-response `is_admin`

## Quick Start

```bash
pip install odoo-mcp-gateway

# Copy and edit config files
cp config/restrictions.yaml.example config/restrictions.yaml
cp config/model_access.yaml.example config/model_access.yaml
cp config/rbac.yaml.example config/rbac.yaml

# Set environment variables
export ODOO_URL=http://localhost:8069
export ODOO_DB=mydb

# Run (stdio mode for Claude Desktop / Claude Code)
python -m odoo_mcp_gateway

# Or HTTP mode for web clients
MCP_TRANSPORT=streamable-http python -m odoo_mcp_gateway
```

### Claude Desktop Configuration

Add to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "odoo": {
      "command": "python",
      "args": ["-m", "odoo_mcp_gateway"],
      "env": {
        "ODOO_URL": "http://localhost:8069",
        "ODOO_DB": "mydb"
      }
    }
  }
}
```

### Claude Code Configuration

```bash
# Add as MCP server
claude mcp add odoo -- python -m odoo_mcp_gateway
```

### Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `ODOO_URL` | `http://localhost:8069` | Odoo server URL |
| `ODOO_DB` | *(required)* | Odoo database name |
| `MCP_TRANSPORT` | `stdio` | Transport mode (`stdio` or `streamable-http`) |
| `MCP_HOST` | `127.0.0.1` | HTTP host (streamable-http mode) |
| `MCP_PORT` | `8080` | HTTP port (streamable-http mode) |
| `MCP_LOG_LEVEL` | `INFO` | Logging level |
| `CONFIG_DIR` | `.` | Directory for YAML config files |
| `SESSION_TIMEOUT_SECONDS` | `1800` | Session inactivity timeout |
| `MAX_CONCURRENT_SESSIONS` | `100` | Maximum concurrent sessions |
| `RATE_LIMIT_GLOBAL` | `60` | Requests per minute (global) |
| `RATE_LIMIT_WRITE` | `20` | Write operations per minute |

## Security

### Two-Layer Security Model

1. **MCP gateway restrictions** (YAML config + hardcoded guardrails) — blocks sensitive models, dangerous methods, privileged fields *before* any Odoo call is made
2. **Odoo's built-in ACLs** — enforces per-user access on actual records via `ir.model.access` and `ir.rule`

### Model Restriction Tiers

| Tier | Effect | Example |
|------|--------|---------|
| `always_blocked` | Nobody can access, including admins | `ir.config_parameter`, `res.users.apikeys` |
| `admin_only` | Only admin users | `ir.model`, `ir.model.fields` |
| `admin_write_only` | Read OK for all, write needs admin | `res.company`, `res.currency` |

### Hardcoded Safety Guardrails

These cannot be overridden by YAML configuration:

**Blocked models** (32): `ir.config_parameter`, `res.users`, `res.users.apikeys`, `res.users.log`, `ir.cron`, `ir.module.module`, `ir.model.access`, `ir.rule`, `ir.mail_server`, `ir.ui.view`, `ir.actions.server`, `ir.logging`, `ir.attachment`, `ir.exports`, `ir.exports.line`, `iap.account`, `auth.totp.wizard`, `auth.totp.device`, `payment.token`, `payment.provider`, `base.automation`, `digest.digest`, `res.config.settings`, `change.password.wizard`, `change.password.user`, `base.module.update`, `base.module.upgrade`, `base.module.uninstall`, `fetchmail.server`, `bus.bus`, `mail.mail`, `mail.template`

**Read-only models** (8): `mail.message`, `mail.followers`, `mail.activity`, `discuss.channel`, `mail.notification`, `mail.compose.message`, `mail.alias`, `discuss.channel.member` (reads allowed, writes blocked for everyone)

**Blocked write fields** (10): `password`, `password_crypt`, `groups_id`, `totp_secret`, `signup_token`, `signup_type`, `signup_expiration`, `api_key`, `share`, `active`

**Blocked methods** (29): `sudo`, `with_user`, `with_company`, `with_context`, `with_env`, `with_prefetch`, `_auto_init`, `_sql`, `_register_hook`, `_write`, `_create`, `_read`, `_setup_base`, `_setup_fields`, `_setup_complete`, `init`, `_table_query`, `_read_group_raw`, `name_create`, `load`, `import_data`, `export_data`, `flush_recordset`, `invalidate_recordset`, `_search_panel_select_range`, `_search_panel_select_multi_range`, `_search_panel_domain_image`, `_search`, `_read_progress_bar`

### Additional Security Features

- **Brute-force protection** — per-username lockout (5 fails → 5 min) AND per-source IP/connection lockout (30 fails → 15 min, blocks username-rotation attacks). Lockouts have fixed duration — cannot be extended by additional attempts (DoS-resistant).
- **Credential wrapper** — passwords/session IDs stored in a `Credential` class with leak-safe `__repr__`/`__str__`, explicit `.reveal()` for use, and `.clear()` on close
- **Server-side admin verification** — `is_admin` is re-verified via `has_group('base.group_system')` after authentication, defending against tampered auth responses
- **Private method guard** — underscore-prefixed methods (`_compute_*`, `_inverse_*`, etc.) blocked for everyone including admin unless explicitly whitelisted
- **Rate limiting** — per-session token bucket with separate global and write budgets
- **RBAC** — tool-level access control by user group, field-level response filtering, transparent drop reporting via `return_dropped=True`
- **Input validation** — model names, method names, field names, domain filters, ORDER BY clauses, groupby with temporal operators, write values (size/depth/type)
- **IDOR protection** — plugin tools scope data access to the authenticated user
- **Audit logging** — structured JSON logs for all allowed and denied operations
- **Error sanitization** — strips internal URLs, SQL fragments, file paths, stack traces from error messages
- **XXE protection** — XML-RPC responses parsed with `defusedxml`
- **Domain validation** — Odoo domain filters validated for operators, field names, value types, nesting depth, and list sizes
- **Session-expiry retry** — JSON-RPC retries only on `OdooSessionExpiredError`, not generic auth errors (no double round-trips on access denials)

## Authentication

Three stock Odoo auth methods — no custom addon needed:

| Method | Protocol | Use Case |
|--------|----------|----------|
| `api_key` | XML-RPC | Server-to-server, CI/CD pipelines |
| `password` | JSON-RPC | Interactive users, Claude Desktop |
| `session` | JSON-RPC | Reuse existing browser session (development) |

```
# Example: login via the MCP tool
> login(method="password", username="admin", credential="admin", database="mydb")
```

## Core MCP Tools (13)

| Tool | Description |
|------|-------------|
| `login` | Authenticate with Odoo (api_key / password / session) |
| `list_models` | List accessible models with metadata and keyword filter |
| `get_model_fields` | Get field definitions for a model with optional filter |
| `search_read` | Search records with domain filters, field selection, ordering |
| `get_record` | Get a single record by ID |
| `search_count` | Count matching records |
| `create_record` | Create a new record (supports `dry_run` for validation-only) |
| `update_record` | Update existing record (supports `dry_run` for validation-only) |
| `delete_record` | Delete a single record by ID (supports `dry_run`) |
| `read_group` | Aggregated grouped reads with temporal operators (`date:month`, `date:quarter`, etc.) |
| `get_defaults` | Preview Odoo default values before `create_record` |
| `get_onchange` | Preview field side effects (with RBAC filtering) |
| `execute_method` | Call allowed model methods (supports `dry_run`) |

## Workflow Tools (2)

| Tool | Description |
|------|-------------|
| `get_create_requirements` | Get required fields and validation rules before creating a record |
| `get_record_actions` | Get available workflow actions for an existing record |

## MCP Resources (6)

| URI | Description |
|-----|-------------|
| `odoo://models` | List all accessible models |
| `odoo://models/{name}` | Model detail with field definitions |
| `odoo://record/{model}/{id}` | Single record data with RBAC field filtering |
| `odoo://schema/{model}` | Field schema with type info and importance ranking |
| `odoo://categories` | Model categories with counts |
| `odoo://workflow/{model}` | Workflow definition with stages and actions for a model |

## MCP Prompts (12)

| Prompt | Description |
|--------|-------------|
| `analyze_model` | Comprehensive model structure analysis |
| `explore_data` | Natural language data exploration guide |
| `create_workflow` | Guide through model-specific workflows |
| `compare_records` | Side-by-side record comparison |
| `generate_report` | Analytical report generation |
| `discover_custom_modules` | Find and understand custom modules |
| `debug_access` | Troubleshoot access and permission issues |
| `workflow_guide` | Step-by-step workflow execution guide for a model |
| `record_creation_guide` | Guided record creation with field validation |
| `bulk_operations` | Guide for performing bulk operations safely |
| `field_mapping` | Map fields between Odoo versions (v17/v18/v19) |
| `data_migration` | Guide for migrating data between models or versions |

## Built-in Domain Plugins

### HR Plugin
| Tool | Description |
|------|-------------|
| `check_in` | Record attendance check-in |
| `check_out` | Record attendance check-out |
| `get_my_attendance` | View attendance records (with month filter) |
| `get_my_leaves` | View leave requests (with state filter) |
| `request_leave` | Submit a leave request |
| `get_my_profile` | View employee profile |

### Sales Plugin
| Tool | Description |
|------|-------------|
| `get_my_quotations` | List quotations/orders (with state filter) |
| `get_order_details` | Full order details with line items |
| `confirm_order` | Confirm a draft/sent quotation |
| `get_sales_summary` | Aggregated sales statistics (with period filter) |

### Project Plugin
| Tool | Description |
|------|-------------|
| `get_my_tasks` | List assigned tasks (with state/project filter) |
| `get_project_summary` | Project stats: task counts by stage, overdue |
| `update_task_stage` | Move a task to a different stage |

### Helpdesk Plugin
| Tool | Description |
|------|-------------|
| `get_my_tickets` | List assigned tickets (with state/priority filter) |
| `create_ticket` | Create a new helpdesk ticket |
| `update_ticket_stage` | Move a ticket to a different stage |

## Custom Module Support

Add custom Odoo modules without writing Python code. Edit `model_access.yaml`:

```yaml
custom_models:
  full_crud:
    - custom.delivery.route
    - custom.warehouse.zone
  read_only:
    - custom.delivery.log

allowed_methods:
  custom.delivery.route:
    - action_dispatch
    - action_complete
    - action_cancel
```

Then all CRUD tools (`search_read`, `create_record`, `update_record`, `delete_record`) and `execute_method` work on the custom models with full security enforcement.

## Plugin System

Extend the gateway with pip-installable plugins:

```python
from odoo_mcp_gateway.plugins.base import OdooPlugin

class ManufacturingPlugin(OdooPlugin):
    @property
    def name(self) -> str:
        return "manufacturing"

    @property
    def required_odoo_modules(self) -> list[str]:
        return ["mrp"]

    @property
    def required_models(self) -> list[str]:
        return ["mrp.production", "mrp.bom"]

    def register(self, server, context):
        @server.tool()
        async def get_production_orders(...):
            ...
```

Register via `pyproject.toml` entry points:

```toml
[project.entry-points."odoo_mcp_gateway.plugins"]
manufacturing = "my_package:ManufacturingPlugin"
```

## Configuration Files

| File | Purpose |
|------|---------|
| `config/restrictions.yaml` | Model/method/field block lists (3 tiers) |
| `config/model_access.yaml` | Per-model access policies, allowed methods, sensitive fields |
| `config/rbac.yaml` | Role-based tool access and field filtering by group |
| `config/gateway.yaml` | Server, connection, auth settings |

All files have `.example` templates with extensive inline documentation. Copy and customize:

```bash
cp config/restrictions.yaml.example config/restrictions.yaml
cp config/model_access.yaml.example config/model_access.yaml
cp config/rbac.yaml.example config/rbac.yaml
```

### Example: Restrict a Model

```yaml
# restrictions.yaml
restrictions:
  always_blocked:
    - my.secret.model
  admin_only:
    - hr.salary.rule
  admin_write_only:
    - res.company
  blocked_write_fields:
    - password_crypt
    - api_key
    - totp_secret
```

### Example: RBAC by Group

```yaml
# rbac.yaml
rbac:
  tool_group_requirements:
    delete_record:
      - base.group_system
    execute_method:
      - base.group_erp_manager
  sensitive_fields:
    hr.employee:
      salary:
        required_group: hr.group_hr_manager
```

## Docker

```bash
cp .env.example .env   # Edit with your Odoo settings
docker compose up
```

Services:
- **MCP Gateway** — port 8080 (streamable-http mode)
- **Odoo 18** — internal only (no host port exposed by default)
- **PostgreSQL** — internal only

The gateway runs as a non-root user in a minimal Python image.

## CLI Tools

```bash
# Test Odoo connectivity
odoo-mcp-tools test-connection --url http://localhost:8069

# Validate all YAML config files
odoo-mcp-tools validate-config --config-dir config

# List configured model access policies
odoo-mcp-tools list-models --config-dir config
```

## Development

```bash
git clone https://github.com/parth-unjiya/odoo-mcp-gateway.git
cd odoo-mcp-gateway
pip install -e ".[dev]"

# Run tests
pytest tests/ -v

# Run with coverage
pytest tests/ --cov=odoo_mcp_gateway --cov-report=term-missing

# Lint
ruff check src/ tests/

# Type check (strict mode)
mypy src/
```

### Source Layout

```
src/odoo_mcp_gateway/
├── __main__.py                  # Entry point (stdio + HTTP)
├── server.py                    # FastMCP server setup, tool registration
├── config.py                    # Pydantic settings (env + .env)
├── client/
│   ├── base.py                  # OdooClientBase ABC, AuthResult
│   ├── jsonrpc.py               # JSON-RPC client (session auth)
│   ├── xmlrpc.py                # XML-RPC client (API key auth, defusedxml)
│   └── exceptions.py            # OdooError hierarchy (7 types)
├── core/
│   ├── auth/manager.py          # 3 auth strategies
│   ├── connection/manager.py    # Circuit breaker + retry
│   ├── version/                 # Odoo 17/18/19 detection + adapters
│   ├── workflow/
│   │   ├── definitions.py      # WorkflowDef, StateDef, TransitionDef dataclasses
│   │   ├── registry.py         # Workflow registration and lookup
│   │   └── stock_workflows/    # Built-in workflows (sale, purchase, HR, etc.)
│   ├── security/
│   │   ├── restrictions.py      # 3-tier model/method restrictions + hardcoded guardrails
│   │   ├── rbac.py              # Tool access + field filtering
│   │   ├── middleware.py        # Security pipeline + security_gate()
│   │   ├── rate_limit.py        # Token bucket rate limiter
│   │   ├── audit.py             # Structured audit logging
│   │   ├── sanitizer.py         # Error message sanitization
│   │   └── config_loader.py     # YAML config → Pydantic models
│   └── discovery/
│       ├── model_registry.py    # ir.model auto-discovery
│       ├── field_inspector.py   # fields_get with TTL cache
│       └── suggestions.py       # Category search + related models
├── tools/
│   ├── auth.py                  # login tool
│   ├── schema.py                # list_models, get_model_fields
│   ├── crud.py                  # search_read, create/update/delete, execute_method
│   └── workflow.py              # get_create_requirements, get_record_actions
├── resources/handlers.py        # 6 MCP resources (odoo:// URIs)
├── prompts/handlers.py          # 12 MCP prompt templates
├── plugins/
│   ├── base.py, registry.py     # Plugin ABC + entry_point discovery
│   └── core/                    # Built-in plugins (HR, Sales, Project, Helpdesk)
├── cli/tools.py                 # CLI: test-connection, validate-config
└── utils/                       # Domain builder, formatting, token budget
```

## Testing

**1,476 tests passing, 93% code coverage**, mypy strict clean, ruff clean:

```
tests/unit/
├── client/          # JSON-RPC, XML-RPC, auth manager, XXE protection
├── security/        # Restrictions, RBAC, audit, rate limit, sanitizer, security_gate
├── discovery/       # Model registry, field inspector, suggestions
├── tools/           # All 13 MCP tools + input validation + dry_run
├── plugins/         # Plugin system + 4 domain plugins + IDOR protection
└── cli/             # CLI utility tools
```

```bash
# Run all tests
pytest tests/ -v

# Run specific area
pytest tests/unit/security/ -v
pytest tests/unit/tools/ -v
pytest tests/unit/plugins/ -v

# Coverage report
pytest tests/ --cov=odoo_mcp_gateway --cov-report=html
```

## Error Handling

All Odoo errors are classified into 7 types:

| Error | Cause |
|-------|-------|
| `OdooConnectionError` | Cannot reach Odoo server |
| `OdooAuthError` | Invalid credentials |
| `OdooAccessError` | ir.model.access denied |
| `OdooValidationError` | Field validation failure |
| `OdooUserError` | Business logic error |
| `OdooMissingError` | Record not found |
| `OdooVersionError` | Unsupported Odoo version |

All error messages are sanitized before reaching the MCP client — internal URLs, SQL fragments, file paths, and stack traces are automatically stripped.

## Known Limitations

- **XML-RPC credential handling**: When using API key authentication (XML-RPC), the credential is sent with every RPC call as required by the protocol. Use HTTPS in production. (Note: passwords are stored in a `Credential` wrapper that prevents `repr()`/traceback leakage.)
- **HTTP mode session isolation**: `streamable-http` transport currently has known session isolation limitations — the `_current_session_key` ContextVar is set inside the login tool but subsequent tool calls from different request contexts may fall back to the first available session. **Deploy HTTP mode as single-tenant only** (one user per server process) until per-request middleware lands in v0.3.0. stdio mode is single-session by design and unaffected.

## Contributing

1. Fork the repository
2. Create a feature branch (`git checkout -b feature/my-feature`)
3. Make your changes with tests
4. Ensure all checks pass: `pytest && ruff check src/ tests/ && mypy src/`
5. Submit a pull request

## License

[MIT](LICENSE)
