Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/db-stable-1-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@unclecat/mcp-multi-db": major
---

1.0.0 marks the stable API. Nothing changes for you since 0.0.8: same tools, arguments, config file and env vars. From now on releases follow [semver](https://github.com/unclecatvn/mcp/blob/master/CONTRIBUTING.md#versioning) strictly, so a removed or renamed tool, argument, error code or env var, or a changed default, only ships in a new major. Install with `npx -y @unclecat/mcp-multi-db@1` to get every 1.x fix and feature without breaking changes.
5 changes: 5 additions & 0 deletions .changeset/obsidian-stable-1-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@unclecat/mcp-obsidian": major
---

1.0.0 marks the stable API. Nothing changes for you since 0.0.2: same tools, arguments and env vars. From now on releases follow [semver](https://github.com/unclecatvn/mcp/blob/master/CONTRIBUTING.md#versioning) strictly, so a removed or renamed tool, argument, error code or env var, or a changed default, only ships in a new major. Install with `npx -y @unclecat/mcp-obsidian@1` (instead of `@latest`) to get every 1.x fix and feature without breaking changes.
13 changes: 13 additions & 0 deletions .changeset/odoo-review-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@unclecat/mcp-odoo": major
---

Breaking: `ODOO_INPUT_INVALID` is gone (validation errors now read `Input validation error: ...`) and `search_read` defaults to `limit: 100`.

- Fix `name_search` with a `domain` on Odoo 19 (`unexpected keyword argument 'args'`): name and domain are now sent positionally, which works on 18 and 19.
- Fix the request timeout not covering the response body read; a stalled body now fails with `ODOO_TRANSPORT_FAILED` instead of hanging.
- `search_read` defaults to `limit: 100` and returns `limit_reached: true` when a page is full.
- Tools carry MCP annotations (`readOnlyHint` on reads, `destructiveHint` on `write` / `unlink` / `call_method`).
- Move to `McpServer.registerTool` with zod schemas (single source of truth); unknown arguments are still rejected. Validation failures now read `Input validation error: ...` instead of `[ODOO_INPUT_INVALID]`.
- Trim tool descriptions and instructions (~5.8k → ~3.3k tokens per session); instructions now fit under the ~2 KB hosts truncate at.
- Remove the unused `index.d.ts` (it declared exports `index.js` never had).
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Packages follow [semantic versioning](https://semver.org), so users can pin a ma

If a PR changes shipped code but has no changeset, the release workflow adds a `patch` changeset from the commit subject, so Dependabot updates still get released. Breaking changes always need a hand-written `major` changeset.

All packages are still `0.0.x`. `1.0.0` is planned together with the Odoo refactor, and from then on the table above applies strictly.
Every package reached `1.0.0` together with the Odoo refactor, and the table above now applies strictly: anything listed under `major` waits for the next major version, and the READMEs pin installs to the current major (`@1`).

## Releasing manually (maintainer)

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ Each package has its own README with full configuration, a tool reference and tr
Every server runs through `npx`, so there is nothing to install or build. With [Claude Code](https://docs.anthropic.com/en/docs/claude-code):

```bash
claude mcp add odoo -e ODOO_PROD_URL=https://erp.example.com -e ODOO_PROD_DB=production -e ODOO_PROD_USERNAME=admin -e ODOO_PROD_API_KEY=your-api-key -- npx -y @unclecat/mcp-odoo
claude mcp add odoo -e ODOO_PROD_URL=https://erp.example.com -e ODOO_PROD_DB=production -e ODOO_PROD_USERNAME=admin -e ODOO_PROD_API_KEY=your-api-key -- npx -y @unclecat/mcp-odoo@1
```

```bash
claude mcp add obsidian -e OBSIDIAN_MAIN_PATH=/absolute/path/to/Vault -- npx -y @unclecat/mcp-obsidian
claude mcp add obsidian -e OBSIDIAN_MAIN_PATH=/absolute/path/to/Vault -- npx -y @unclecat/mcp-obsidian@1
```

```bash
claude mcp add multi-db -e MCP_DB_CONFIG=/absolute/path/to/mcp-db.config.json -- npx -y @unclecat/mcp-multi-db
claude mcp add multi-db -e MCP_DB_CONFIG=/absolute/path/to/mcp-db.config.json -- npx -y @unclecat/mcp-multi-db@1
```

For Claude Desktop, Cursor and VS Code, see the configuration section of each package README.
Expand Down
6 changes: 3 additions & 3 deletions db/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ The `$schema` line gives you autocompletion and validation in VS Code and JetBra
**Claude Code**

```bash
claude mcp add multi-db -e MCP_DB_CONFIG=/absolute/path/to/mcp-db.config.json -- npx -y @unclecat/mcp-multi-db
claude mcp add multi-db -e MCP_DB_CONFIG=/absolute/path/to/mcp-db.config.json -- npx -y @unclecat/mcp-multi-db@1
```

**Claude Desktop, Cursor, Windsurf** (`claude_desktop_config.json`, `.cursor/mcp.json` or `.mcp.json`)
Expand All @@ -89,7 +89,7 @@ claude mcp add multi-db -e MCP_DB_CONFIG=/absolute/path/to/mcp-db.config.json --
"mcpServers": {
"multi-db": {
"command": "npx",
"args": ["-y", "@unclecat/mcp-multi-db"],
"args": ["-y", "@unclecat/mcp-multi-db@1"],
"env": { "MCP_DB_CONFIG": "/absolute/path/to/mcp-db.config.json" }
}
}
Expand All @@ -104,7 +104,7 @@ claude mcp add multi-db -e MCP_DB_CONFIG=/absolute/path/to/mcp-db.config.json --
"multi-db": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@unclecat/mcp-multi-db"],
"args": ["-y", "@unclecat/mcp-multi-db@1"],
"env": { "MCP_DB_CONFIG": "${workspaceFolder}/mcp-db.config.json" }
}
}
Expand Down
6 changes: 3 additions & 3 deletions db/README.vi.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Dòng `$schema` giúp bạn có autocomplete và validation trong VS Code và c
**Claude Code**

```bash
claude mcp add multi-db -e MCP_DB_CONFIG=/absolute/path/to/mcp-db.config.json -- npx -y @unclecat/mcp-multi-db
claude mcp add multi-db -e MCP_DB_CONFIG=/absolute/path/to/mcp-db.config.json -- npx -y @unclecat/mcp-multi-db@1
```

**Claude Desktop, Cursor, Windsurf** (`claude_desktop_config.json`, `.cursor/mcp.json` hoặc `.mcp.json`)
Expand All @@ -89,7 +89,7 @@ claude mcp add multi-db -e MCP_DB_CONFIG=/absolute/path/to/mcp-db.config.json --
"mcpServers": {
"multi-db": {
"command": "npx",
"args": ["-y", "@unclecat/mcp-multi-db"],
"args": ["-y", "@unclecat/mcp-multi-db@1"],
"env": { "MCP_DB_CONFIG": "/absolute/path/to/mcp-db.config.json" }
}
}
Expand All @@ -104,7 +104,7 @@ claude mcp add multi-db -e MCP_DB_CONFIG=/absolute/path/to/mcp-db.config.json --
"multi-db": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@unclecat/mcp-multi-db"],
"args": ["-y", "@unclecat/mcp-multi-db@1"],
"env": { "MCP_DB_CONFIG": "${workspaceFolder}/mcp-db.config.json" }
}
}
Expand Down
8 changes: 4 additions & 4 deletions obsidian/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ The server works on the `.md` files in your vault folder. Obsidian notices chang
claude mcp add obsidian -s user \
-e OBSIDIAN_MAIN_PATH="/absolute/path/to/Vault" \
-e OBSIDIAN_MAIN_MODE=readwrite \
-- npx -y @unclecat/mcp-obsidian@latest
-- npx -y @unclecat/mcp-obsidian@1
```

`@latest` makes `npx` check for a new release every time the server starts.
`@1` makes `npx` pick up every new 1.x release when the server starts, and never a breaking 2.0.

### Claude Desktop, Cursor, Windsurf

Expand All @@ -48,7 +48,7 @@ Add the server to `claude_desktop_config.json`, `.cursor/mcp.json` or the projec
"mcpServers": {
"obsidian": {
"command": "npx",
"args": ["-y", "@unclecat/mcp-obsidian@latest"],
"args": ["-y", "@unclecat/mcp-obsidian@1"],
"env": {
"OBSIDIAN_MAIN_PATH": "/absolute/path/to/Vault",
"OBSIDIAN_MAIN_MODE": "readwrite"
Expand All @@ -68,7 +68,7 @@ In `.vscode/mcp.json` (VS Code 1.102+), the top-level key is `servers`:
"obsidian": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@unclecat/mcp-obsidian@latest"],
"args": ["-y", "@unclecat/mcp-obsidian@1"],
"env": { "OBSIDIAN_MAIN_PATH": "/absolute/path/to/Vault" }
}
}
Expand Down
8 changes: 4 additions & 4 deletions obsidian/README.vi.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ Server làm việc trên các file `.md` trong thư mục vault của bạn. Obs
claude mcp add obsidian -s user \
-e OBSIDIAN_MAIN_PATH="/absolute/path/to/Vault" \
-e OBSIDIAN_MAIN_MODE=readwrite \
-- npx -y @unclecat/mcp-obsidian@latest
-- npx -y @unclecat/mcp-obsidian@1
```

`@latest` khiến `npx` kiểm tra bản phát hành mới mỗi lần server khởi động.
`@1` khiến `npx` tự lấy bản 1.x mới nhất mỗi lần server khởi động, và không bao giờ nhảy sang bản 2.0 có thay đổi không tương thích.

### Claude Desktop, Cursor, Windsurf

Expand All @@ -48,7 +48,7 @@ Thêm server vào `claude_desktop_config.json`, `.cursor/mcp.json` hoặc `.mcp.
"mcpServers": {
"obsidian": {
"command": "npx",
"args": ["-y", "@unclecat/mcp-obsidian@latest"],
"args": ["-y", "@unclecat/mcp-obsidian@1"],
"env": {
"OBSIDIAN_MAIN_PATH": "/absolute/path/to/Vault",
"OBSIDIAN_MAIN_MODE": "readwrite"
Expand All @@ -68,7 +68,7 @@ Trong `.vscode/mcp.json` (VS Code 1.102+), key cấp cao nhất là `servers`:
"obsidian": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@unclecat/mcp-obsidian@latest"],
"args": ["-y", "@unclecat/mcp-obsidian@1"],
"env": { "OBSIDIAN_MAIN_PATH": "/absolute/path/to/Vault" }
}
}
Expand Down
40 changes: 19 additions & 21 deletions odoo/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,43 +23,41 @@ Plain JavaScript (ESM). No build step — `bin/mcp-odoo` shells straight to `ind

### Entry-point flow

`index.js` → `OdooMCPServer` (`mcpServer.js`) → `ClientRegistry` → `OdooClient`
`index.js` → `OdooMCPServer` (`mcpServer.js`) → `registerTools(McpServer, Map<name, OdooClient>)`

### Modules

| File | Responsibility |
| -------------------------- | ---------------------------------------------------------------------------------- |
| `index.js` | Shebang + spawns `OdooMCPServer.run()` |
| `mcpServer.js` | MCP wiring: parses env, registers tool handlers, sets `serverInfo.instructions` |
| `lib/config.js` | `parseEnv()` — discovers `ODOO_<NAME>_*` blocks; never throws on per-connection error |
| `lib/errors.js` | `McpOdooError` subclasses + `formatErrorForMcp()` for stable tool error envelopes |
| `lib/validators.js` | zod schemas for every tool's input; `parseOrThrow` turns failures into `ValidationError` |
| `lib/client.js` | `OdooClient` — JSON-RPC fetch wrapper, lazy `authenticate()` with in-flight dedup |
| `lib/clientRegistry.js` | One `OdooClient` per configured connection; resolves by name |
| `lib/toolHandlers.js` | `ToolHandlers.toolDescriptors()` + `.dispatch(name, args)` for 7 tools |
| `lib/instructions.js` | The Odoo cheatsheet sent on `initialize` |
| File | Responsibility |
| --------------------- | -------------------------------------------------------------------------------------- |
| `index.js` | Shebang + runs `OdooMCPServer.run()` |
| `mcpServer.js` | Parses env, logs to stderr, builds one `OdooClient` per connection, creates `McpServer` |
| `lib/config.js` | `parseEnv()` — discovers `ODOO_<NAME>_*` blocks; never throws on per-connection error |
| `lib/errors.js` | `OdooError(code, message)` (message pre-prefixed `[CODE]`) + `fromOdooError()` |
| `lib/client.js` | `OdooClient` — JSON-RPC fetch wrapper, lazy `authenticate()` with in-flight dedup |
| `lib/toolHandlers.js` | `registerTools()` — the 10 tools, their zod input schemas and annotations |
| `lib/instructions.js` | The cheatsheet sent on `initialize` (keep under 2 KB — hosts truncate it) |

### Tools registered

`list_connections`, `fields_get`, `search_read`, `create`, `write`, `unlink`, `call_method`.
Read (`readOnlyHint`): `list_connections`, `fields_get`, `search_read`, `search_count`, `name_search`, `read_group`.
Write: `create`, `write`, `unlink`, `call_method` (the last three `destructiveHint`).

### Connection configuration

`ODOO_<NAME>_<FIELD>` env vars, discovered by the `_URL` anchor. Fields: `URL`, `DB`, `USERNAME`, plus exactly one of `API_KEY` / `PASSWORD`. Bad entries are logged and skipped — server keeps running.
`ODOO_<NAME>_<FIELD>` env vars, discovered by the `_URL` anchor. Fields: `URL`, `DB`, `USERNAME`, plus exactly one of `API_KEY` / `PASSWORD`, optional `TIMEOUT_MS`. Bad entries are logged and skipped — server keeps running.

### Important conventions to preserve

- **MCP stdio**: never `console.log` — that corrupts the protocol stream. Logging goes to **stderr** via the structured logger in `mcpServer.js`.
- **Errors**: tool failures must come back through `formatErrorForMcp()` so the host sees `{ isError: true, content: [{ type: "text", text: "[CODE] message" }] }`.
- **Validation**: every tool handler validates with `parseOrThrow(schema, args, toolName)` before touching the registry.
- **MCP stdio**: never `console.log` — that corrupts the protocol stream. Logging goes to **stderr** via `this.log` in `mcpServer.js`.
- **Errors**: throw `OdooError`; the SDK turns any thrown error into `{ isError: true, content: [{ type: "text", text: err.message }] }`, and the message already starts with `[CODE]`.
- **Validation**: input schemas are `z.object({...}).strict()` so unknown keys are rejected, not silently dropped. Odoo validates model/method names itself — don't add regexes that cost schema tokens on every tool.
- **Odoo 18 vs 19 signatures**: pass parameters whose names differ between versions positionally (e.g. `name_search`'s 2nd param is `args` in 18, `domain` in 19).
- **Secrets**: `list_connections` exposes `authType` but never `secret`. `OdooClient.describe()` is the single source of truth for that contract — keep it lean.
- **Auth lazy + deduped**: don't call `_rpc("common","authenticate",...)` directly from handlers; always go through `client.callKw()` which guarantees one in-flight authenticate even under concurrent first calls.

### Adding a new tool

1. Add input schema to `lib/validators.js`.
2. Add a `case` and an `async _xxx(args)` method to `lib/toolHandlers.js`.
3. Add a descriptor to `toolDescriptors()` with a description that includes an example.
4. Add a test to `test/unit/toolHandlers.test.js`.
1. Add a `server.registerTool(...)` call in `lib/toolHandlers.js` with a strict zod schema, `annotations`, and a short description with one EXAMPLE.
2. Add a test to `test/unit/server.test.js` (stub client via `registerTools`).

If the tool needs new behaviour on `OdooClient`, add a method there and unit-test it in `test/unit/client.test.js` with a stubbed `fetchImpl`.
12 changes: 7 additions & 5 deletions odoo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ The server exposes Odoo's ORM through ten generic tools, so it works with every
- a `call_method` escape hatch for business actions such as confirming orders, posting invoices or running wizards.
- **Several instances.** Configure any number of instances, each with its own credentials and timeout.
- **API-key auth** (recommended) or a password, chosen per instance.
- **Built-in Odoo cheatsheet.** On `initialize` the model receives notes on domain syntax, x2many command tuples, field formats and error codes, so it builds correct calls without trial and error.
- **Built-in Odoo cheatsheet.** On `initialize` the model receives a short note on the workflow, x2many command tuples, field formats and error codes. It stays under the ~2 KB that hosts such as Claude Code truncate at; domain syntax and business-action examples live in each tool's description.
- **Stable error codes.** Odoo exceptions are mapped to codes such as `ODOO_ACCESS_DENIED` and `ODOO_USER_ERROR`, so the model knows whether to fix the call, retry or stop.

## Quick start
Expand All @@ -36,7 +36,7 @@ claude mcp add odoo \
-e ODOO_PROD_DB=production \
-e ODOO_PROD_USERNAME=admin@example.com \
-e ODOO_PROD_API_KEY=your-api-key \
-- npx -y @unclecat/mcp-odoo
-- npx -y @unclecat/mcp-odoo@1
```

### Claude Desktop, Cursor, Windsurf
Expand All @@ -48,7 +48,7 @@ Add the server to `claude_desktop_config.json`, `.cursor/mcp.json` or the projec
"mcpServers": {
"odoo": {
"command": "npx",
"args": ["-y", "@unclecat/mcp-odoo"],
"args": ["-y", "@unclecat/mcp-odoo@1"],
"env": {
"ODOO_PROD_URL": "https://erp.example.com",
"ODOO_PROD_DB": "production",
Expand Down Expand Up @@ -106,7 +106,7 @@ Every tool except `list_connections` takes a `connection` argument.
|---|---|---|
| `list_connections` | Discover the configured instances, once per session | `{ connections: [{ name, url, db, username, authType, ... }] }` |
| `fields_get` | Inspect a model's fields before writing. Results are cached. | `{ model, fields: { name: { type, required, relation, ... } } }` |
| `search_read` | Query records with a domain, fields, limit and order | `{ model, count, records: [...] }` |
| `search_read` | Query records with a domain, fields, limit and order. `limit` defaults to 100; `limit_reached: true` means there may be more. | `{ model, count, limit_reached?, records: [...] }` |
| `search_count` | Count records without fetching them | `{ model, count }` |
| `name_search` | Fuzzy lookup by display name | `{ model, results: [[id, "name"], ...] }` |
| `read_group` | Group by fields and aggregate, for reports and KPIs | `{ model, count, groups: [...] }` |
Expand All @@ -128,7 +128,6 @@ Errors are returned as `[CODE] message` inside an MCP `isError` result.

| Code | Meaning | What the model should do |
|---|---|---|
| `ODOO_INPUT_INVALID` | Arguments failed schema validation | Fix the call |
| `ODOO_UNKNOWN_CONNECTION` | Unknown `connection` | Call `list_connections` again |
| `ODOO_AUTH_FAILED` | Bad credentials, or a password login for a 2FA user | Stop and ask the operator |
| `ODOO_ACCESS_DENIED` | The user lacks rights on the model or record | Do not retry |
Expand All @@ -138,9 +137,12 @@ Errors are returned as `[CODE] message` inside an MCP `isError` result.
| `ODOO_SERVER_ERROR` | Unexpected Odoo exception | Treat as fatal for this call |
| `ODOO_TRANSPORT_FAILED` | Network, HTTP or timeout error | Safe to retry once |

Arguments that fail the tool schema, including unknown keys, come back as the SDK's `Input validation error: ...` text instead of a code.

## Security

- **Least privilege.** The model can do anything the configured Odoo user can do. Connect it as a dedicated user whose access rights match what you want the assistant to touch.
- **Tool annotations.** Read tools carry `readOnlyHint`; `write`, `unlink` and `call_method` carry `destructiveHint`, so hosts can auto-approve reads and ask before writes.
- **Prefer API keys.** You can revoke an API key without changing the user's password.
- **Plain HTTP.** `http://` URLs are accepted but trigger a startup warning, because credentials would travel in clear text.
- **No secret leaks.** `list_connections` reports the auth *type*, never the key or password.
Expand Down
Loading
Loading