MCP Server
Configure, secure, and monitor the Model Context Protocol server for AI-assisted Odoo management.
The oCore MCP server enables AI assistants to interact with Odoo instances through a standardized protocol. This guide covers server architecture, configuration, security hardening, and monitoring for administrators.
Organization Settings
Manage MCP kill switch, API keys, and audit logs.
Architecture
Transports
oCore exposes the MCP server over two transports:
| Transport | Endpoint | Use Case |
|---|---|---|
| SSE (Server-Sent Events) | /api/mcp/sse | Long-lived connections from desktop clients (Claude, Cursor) |
| Streamable HTTP | /api/mcp/stream | Stateful HTTP sessions with idle timeout |
Both transports share the same MCP server instance and tool registry. Authentication, RBAC, rate limiting, and audit logging apply identically across both.
Per-Session Tool Registry
Tools are not registered globally for all clients. Instead, when a session is established:
- The API key is authenticated and its permissions are loaded.
- Accessible instances are queried based on the key's organization and instance scope.
- Tools are registered on the session, namespaced by instance slug (e.g.,
prod_v17_search_read). - Resources (model list, record access) are registered per instance.
This means each session sees only the tools for the instances it has access to. If an API key is scoped to two instances, the session gets tools for those two instances plus the global ocore_* platform tools.
Tool Naming Convention
Instance tools follow the pattern {normalized_slug}_{tool_suffix}:
- Hyphens in slugs are replaced with underscores:
prod-v17becomesprod_v17 - The slug
ocoreis reserved for platform tools and cannot be used as an instance slug - If two instances produce the same normalized slug, both receive an 8-character UUID suffix to disambiguate
Configuration
The MCP server is configured through the Config struct with the following defaults:
| Setting | Default | Description |
|---|---|---|
MaxInstances | 20 | Maximum instances registered per session |
RateLimitSSE | 100 | Maximum tool calls per minute per key (SSE transport) |
RateLimitStdio | 30 | Maximum tool calls per minute per key (stdio transport) |
KeepAliveInterval | 15 seconds | SSE keep-alive ping interval |
SessionIdleTTL | 30 minutes | Idle timeout for Streamable HTTP sessions |
TrustedProxyCIDRs | (empty) | CIDR ranges of reverse proxies whose X-Forwarded-For headers are trusted |
Trusted Proxy Configuration
If oCore runs behind a reverse proxy (nginx, Caddy, cloud load balancer), configure TrustedProxyCIDRs so the MCP server extracts the real client IP from proxy headers rather than using the proxy's IP:
# Example: trust the local Docker network and Cloudflare ranges
TRUSTED_PROXY_CIDRS=172.17.0.0/16,173.245.48.0/20,103.21.244.0/22When no trusted proxies are configured, X-Forwarded-For and X-Real-IP headers are ignored and the direct connection IP from RemoteAddr is used. This is the safe default.
Kill Switch
The organization-level MCP kill switch (mcp_enabled in organization settings) instantly disables all MCP access for the entire organization:
- New sessions fail authentication
- Existing sessions are blocked on the next tool call (revalidated every 30 seconds)
- The toggle is checked both at session creation and periodically during tool calls
To toggle the kill switch, navigate to Settings > General and toggle the MCP Access switch, or use the API:
curl -X PUT https://ocore.example.com/api/org/settings \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"mcp_enabled": false}'Kill Switch Is Organization-Wide
Disabling MCP access affects all API keys in the organization. Use per-key controls (pause, read-only, instance scoping) for more targeted restrictions.
API Key Scoping
MCP access control is configured per API key. Each key supports the following MCP-specific settings:
Instance Scoping
Restrict a key to specific instances by setting mcp_instance_ids. The key will only see tools for those instances. An empty list grants access to all running instances in the organization.
Project Scoping
Restrict a key to instances belonging to specific projects via mcp_project_ids.
Category Permissions
Set mcp_permissions to a list of tool categories the key can access:
| Category | Tools |
|---|---|
orm | search_read, read, create, write, unlink, copy, execute_method, batch_* |
search | advanced_search, aggregation, distinct_field_values, fuzzy_search |
metadata | list_models, get_fields, get_metadata, get_model_*, get_instance_info |
sql | execute_sql, execute_sql_script, list_tables |
shell | execute_shell, execute_shell_multi, python_repl |
files | file_browse, file_read, file_write, file_create, file_delete, file_tree, search_in_code |
modules | install_module, uninstall_module, upgrade_module, list_modules, module_* |
system | list_users, user_groups, set_user_*, server_info, odoo_logs, scheduled actions |
platform | ocore_list_instances, ocore_list_servers |
An empty permissions list means unrestricted access to all categories.
Read-Only Mode
Set mcp_read_only: true to block all write and destructive operations. The key can still search, read, and inspect schemas.
Active/Pause Toggle
Set mcp_active: false to pause a key without revoking it. Paused keys receive a clear error on every tool call. Resume by setting mcp_active: true.
Tool Allow/Deny Lists
Fine-grained per-tool filtering with mcp_allowlist_mode and mcp_tool_allowlist:
allowmode: Only tools whose suffix is in the list are permitteddenymode: Tools whose suffix is in the list are blocked; all others are allowednonemode (default): No per-tool filtering
IP Allowlist
Set ip_allowlist to restrict connections to specific IP addresses or CIDR ranges. Connections from non-listed IPs are rejected.
Production Guard
Each Odoo instance has a mcp_write_enabled boolean flag (default: false). When disabled, all write and destructive tool calls targeting that instance are blocked, regardless of the API key's permissions.
This provides a safety net for production instances:
- Enable writes only on development and staging instances
- Temporarily enable writes on production for specific maintenance windows
- The flag is checked on every tool call with a fresh DB query when the session cache says blocked
To toggle the production guard:
curl -X PUT https://ocore.example.com/api/instances/{instanceId} \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"mcp_write_enabled": true}'Token Revalidation
API keys are not validated only at session creation. The MCP server periodically revalidates keys during active sessions:
- Interval: Every 30 seconds per key
- Checks performed: Key existence, expiration, owning user active status,
mcp_activeflag, organization kill switch - Fail-open on transient errors: If the database is temporarily unreachable, the call proceeds to avoid breaking sessions on brief outages
- Immediate eviction: When a key is confirmed revoked, expired, or its user is deactivated, the next tool call fails immediately
Call InvalidateAuthCache(keyID) programmatically to force immediate revalidation (e.g., when pausing or revoking a key through the UI).
Monitoring
Health Endpoint
The MCP health endpoint is available at /api/mcp/health and returns basic server status:
curl https://ocore.example.com/api/mcp/health{
"status": "ok",
"server_name": "oCore — Odoo Platform Manager",
"version": "1.0.0"
}This endpoint is unauthenticated and does not expose operational details.
Session Tracking
Active MCP sessions are tracked via an atomic counter. The current session count is available through the authenticated dashboard stats endpoint.
Audit Logs
All MCP tool calls are written to the mcp_audit_log table. Each entry records:
| Field | Description |
|---|---|
organization_id | Organization the key belongs to |
user_id | User who owns the API key |
api_key_id | The API key used |
session_id | MCP session identifier |
tool_name | Full tool name (e.g., prod_v17_search_read) |
tool_category | RBAC category (e.g., orm, sql) |
input_params | Request parameters (sensitive values redacted) |
result_summary | Truncated result (first 500 characters) |
result_bytes | Total response size in bytes |
is_error | Whether the tool call failed |
error_message | Error text (if applicable) |
latency_ms | Execution time in milliseconds |
ip_address | Client IP address |
instance_id | Target instance (null for platform tools) |
High-level events (session start/end) are logged to the main audit_log table with action types mcp_session_started and mcp_session_ended.
Usage Stats
Query MCP usage statistics per API key:
# Get usage stats for a specific key
curl "https://ocore.example.com/api/org/mcp/key-stats/{keyId}" \
-H "Authorization: Bearer $TOKEN"Security Hardening Checklist
Enable the kill switch by default. Only enable MCP access when actively using AI tools. Toggle it off when not in use.
Scope API keys to specific instances. Never use an unscoped key in production. Create separate keys for production (read-only) and development (read-write).
Set read-only mode for monitoring keys. Keys used for dashboards, reporting, or read-heavy workflows should be marked mcp_read_only: true.
Use category restrictions. If a key only needs to inspect schemas, grant only metadata permission. If it only needs ORM access, grant only orm.
Configure IP allowlists. Restrict keys to known IP ranges, especially for production instance access.
Keep mcp_write_enabled off for production instances. Only enable it during planned maintenance windows and disable it immediately after.
Configure trusted proxy CIDRs. If oCore runs behind a reverse proxy, set TrustedProxyCIDRs so IP-based access controls work correctly.
Set key expiration dates. Avoid indefinite keys. Use expiration dates and rotate keys regularly.
Monitor audit logs. Review mcp_audit_log entries regularly for unexpected tool calls, high error rates, or unfamiliar IP addresses.
Use per-tool allow/deny lists. For high-security environments, use allowlist mode to explicitly permit only the tools each key needs.
Rate Limiting
Per-key rate limiting prevents any single API key from overwhelming the server. Limits are enforced per minute using a token bucket algorithm:
| Transport | Default Limit |
|---|---|
| SSE | 100 calls/minute |
| Stdio | 30 calls/minute |
When a key exceeds its limit, tool calls return a rate limit exceeded error until the window resets. Rate limiter state is in-memory and automatically cleaned up for keys idle longer than 30 minutes.
Troubleshooting
MCP sessions not connecting
- Check that
mcp_enabledistruein organization settings - Verify the API key has not expired or been revoked
- Check that the owning user account is active
- Verify the SSE or Streamable HTTP endpoint is reachable (not blocked by firewall/WAF)
No tools appearing in session
- Confirm at least one instance has
status: runningin the organization - Check that the API key is not scoped to instances that are stopped or deleted
- Verify the key's
mcp_activeflag istrue
Writes blocked unexpectedly
- Check the instance's
mcp_write_enabledflag - Check the API key's
mcp_read_onlyflag - Verify the key's category permissions include write-capable categories
- For destructive operations, ensure
confirm: trueis in the tool arguments
IP-related rejections
- Verify the client IP is in the key's
ip_allowlist - If behind a proxy, ensure
TrustedProxyCIDRsis configured so the real client IP is extracted - Check the audit log for the rejected IP address
High latency on tool calls
- Each tool call involves an SSH connection to the server and a
docker execinto the Odoo container, adding approximately 2-3 seconds of baseline latency - Complex ORM operations or large result sets add additional time
- Use
timeout_secondsandmax_response_sizeparameters to bound execution time and response size
OAuth 2.1 Authorization Server
oCore includes a full OAuth 2.1 Authorization Server that enables AI clients like Claude Desktop and ChatGPT to connect to the MCP server without manually copying API keys. Clients discover oCore through standard protocol endpoints, authenticate via the authorization code flow with PKCE, and receive scoped MCP tokens.
Discovery Endpoints
MCP clients use these well-known endpoints to discover the authorization server and its capabilities:
| Endpoint | RFC | Purpose |
|---|---|---|
/.well-known/oauth-protected-resource | RFC 9728 | Tells clients which authorization server protects the MCP resource |
/.well-known/oauth-authorization-server | RFC 8414 | Full metadata: endpoints, supported grant types, PKCE methods |
/.well-known/openid-configuration | OIDC Discovery | Same as above, plus OIDC-specific fields (subject_types_supported, id_token_signing_alg_values_supported) |
/.well-known/jwks.json | RFC 7517 | Public keys for verifying ID tokens (RS256) |
OAuth Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/oauth/authorize | GET | Authorization endpoint (redirects to login/consent) |
/oauth/token | POST | Token exchange (authorization code, refresh token, device code) |
/oauth/register | POST | Dynamic Client Registration (RFC 7591) |
/oauth/introspect | POST | Token introspection (RFC 7662, confidential clients only) |
/oauth/revoke | POST | Token revocation (RFC 7009) |
/oauth/par | POST | Pushed Authorization Request (RFC 9126) |
/oauth/device | POST | Device Authorization Grant (RFC 8628) |
/oauth/device/verify | GET/POST | Device code verification (user-facing) |
/oauth/consent | GET/POST | Consent data and submission |
/oauth/userinfo | GET/POST | OIDC UserInfo |
How Claude Desktop and ChatGPT Connect
When an MCP-compatible AI client connects to oCore:
The client fetches /.well-known/oauth-protected-resource from the MCP server URL to discover the authorization server.
The client fetches /.well-known/oauth-authorization-server to learn the authorization, token, and registration endpoints.
The client dynamically registers itself via POST /oauth/register (if not already registered), receiving a client_id.
The client initiates the authorization code flow with PKCE (code_challenge_method=S256), opening the user's browser to /oauth/authorize.
The user logs into oCore (if not already authenticated), reviews the consent screen, selects an organization, and approves.
oCore redirects back to the client with an authorization code, which the client exchanges for access and refresh tokens at /oauth/token.
Clients may also use CIMD (Client ID Metadata Document) instead of dynamic registration. If the client_id is an HTTPS URL, oCore fetches the metadata document from that URL to resolve client information.
Dynamic Client Registration
Any MCP client can register itself at runtime via POST /oauth/register:
curl -X POST https://ocore.example.com/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My AI Tool",
"redirect_uris": ["https://myapp.example.com/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "none"
}'The server returns a client_id (and client_secret for confidential clients using client_secret_post). Registration is rate-limited to 10 requests per IP per minute.
Supported token_endpoint_auth_method values:
| Method | Type | Description |
|---|---|---|
none | Public | No client secret. Used by desktop apps and CLI tools. |
client_secret_post | Confidential | Client secret sent in POST body. Used by server-side integrations. |
Device Authorization Grant
For CLI tools and environments without a browser redirect, oCore supports RFC 8628 Device Authorization Grant:
The CLI tool sends POST /oauth/device with its client_id. oCore returns a user_code (8 consonants, formatted XXXX-XXXX) and a verification_uri.
The tool displays the user code and verification URL to the operator. A QR code (base64-encoded PNG) is also returned for mobile scanning.
The operator opens the verification URL in a browser, logs in, and enters or confirms the user code. They select an organization and approve.
The CLI tool polls POST /oauth/token with grant_type=urn:ietf:params:oauth:grant-type:device_code. Polling interval starts at 5 seconds; the server responds with slow_down if polled too frequently.
Once approved, the token endpoint returns access and refresh tokens. Device codes expire after 15 minutes.
CORS Configuration
OAuth endpoints use split CORS configurations to balance security with compatibility:
| Endpoint Group | Origins | Credentials | Rationale |
|---|---|---|---|
| Public (token, register, introspect, revoke, .well-known/*) | * (wildcard) | false | Called by external OAuth clients; no cookies needed |
| Session (authorize, consent, par, device/verify) | APP_URL only | true | Loaded by the oCore frontend; requires session cookies |
Public endpoints accept Authorization and Content-Type headers. Session endpoints additionally accept X-CSRF-Token.
Security Headers
All /oauth/* responses include these security headers per RFC 6749 Section 5.1:
Cache-Control: no-store
Pragma: no-cache
X-Content-Type-Options: nosniffThe consent endpoint also sets Referrer-Policy: no-referrer to prevent the request_id from leaking in the Referer header.
JWKS Key Rotation
oCore automatically manages RSA-2048 signing keys for OIDC ID tokens:
- A signing key is generated on first startup if none exists
- Keys are checked hourly and rotated after 90 days
- Rotated keys remain in the JWKS response for 180 days for in-flight token verification
- The JWKS endpoint (
/.well-known/jwks.json) returns all active and recently-rotated public keys - JWKS responses are cached with
Cache-Control: public, max-age=3600