So I ran your repo against my own. I’m sure there are errors and inconsistencies. I’ve done a lot of work mapping the documented and undocumented API, so hopefully you can take the below and make further development
-
# Audit: teamdesk-mcp-v2
**Repository:**
https://github.com/Danielbluz/teamdesk-mcp-v2**Audited against:** TeamDesk REST API v2 protocol (run against ****)
**Date:** 2026-02-14
**Commit:** HEAD of `main` branch at time of audit
---
## Executive Summary
This MCP server wraps the TeamDesk REST API for use with Claude Desktop. The project demonstrates good instincts — rate limiting, caching, input sanitization, API key validation against a TeamDesk table — but **nearly every tool constructs the wrong URL, uses the wrong HTTP method, or both**. As a result, most operations will return 404 or unexpected errors from TeamDesk's API.
The issues below are ordered by severity.
---
## Critical: Wrong API Endpoints
TeamDesk's REST API v2 uses a specific URL structure:
```
https://www.teamdesk.net/secure/api/v2/{db_id}/{table}/{operation}.json
```
The MCP server constructs URLs that don't match this structure. Every tool except `upsert_records` will fail.
### 1. `list_tables` — Wrong endpoint
**Code** (`server.py:707`):
```python
return await http_client.request("GET", token, "tables.json")
```
**Problem:** `tables.json` does not exist in the TeamDesk API.
**Correct endpoint:**
```
GET /api/v2/{db_id}/describe.json
```
This returns database metadata including a `tables` array. Each entry has `id`, `recordName`, `recordsName`, `alias`, `showTab`, `color`.
---
### 2. `describe_table` — Wrong endpoint
**Code** (`server.py:714`):
```python
return await http_client.request("GET", token, f"{table}/columns.json")
```
**Problem:** `columns.json` does not exist in the TeamDesk API.
**Correct endpoint:**
```
GET /api/v2/{db_id}/{table}/describe.json
```
Returns an **object** (not an array) with keys `hideNew`, `allowAdd`, `key`, `nameColumn`, `columns`, `views`, `dashboards`, etc. The `columns` array within the response contains column metadata (name, type, kind, alias, choices, etc.).
---
### 3. `get_record` — Wrong endpoint and URL structure
**Code** (`server.py:751`):
```python
return await http_client.request("GET", token, f"{table}/{record_id}.json", params=params)
```
**Problem:** TeamDesk does not support `/{table}/{id}.json` REST-style resource URLs.
**Correct endpoint:**
```
GET /api/v2/{db_id}/{table}/retrieve.json?id={record_id}
```
Multiple records can be retrieved at once: `?id=1&id=2&id=3`. Also supports key-column lookup: `?{KeyColumn}={value}`.
---
### 4. `create_record` — Wrong endpoint and request format
**Code** (`server.py:759`):
```python
return await http_client.request("POST", token, f"{table}.json", json_data=arguments["data"])
```
**Problem:** `POST /{table}.json` does not exist. Also, the tool accepts a single `data` object, but TeamDesk's create endpoint requires an **array** of record objects.
**Correct endpoint:**
```
POST /api/v2/{db_id}/{table}/create.json
Content-Type: application/json
[{"Name": "Record 1", "Value": 100}]
```
Response:
```json
[{"status": 201, "id": 123, "key": "K001"}]
```
**Suggestion:** Accept either a single object or an array. If a single object is given, wrap it in an array before sending. Return the full response array so the caller gets the new record's `id` and `key`.
---
### 5. `update_record` — Wrong HTTP method, wrong endpoint, wrong request format
**Code** (`server.py:768`):
```python
return await http_client.request("PUT", token, f"{table}/{record_id}.json", json_data=arguments["data"])
```
**Problems (three):**
1. **Wrong method:** TeamDesk uses `POST`, not `PUT`.
2. **Wrong URL:** `/{table}/{id}.json` does not exist.
3. **Wrong body format:** TeamDesk expects an **array** of record objects with `@row.id` for matching.
**Correct:**
```
POST /api/v2/{db_id}/{table}/update.json
Content-Type: application/json
[{"@row.id": 123, "Status": "Complete"}]
```
The `@row.id` field tells TeamDesk which record to update. Alternatively, any key column value can be used for matching.
---
### 6. `delete_record` — Wrong HTTP method and wrong endpoint
**Code** (`server.py:777`):
```python
return await http_client.request("DELETE", token, f"{table}/{record_id}.json")
```
**Problems (two):**
1. **Wrong method:** TeamDesk uses `GET` for delete (this is a known API quirk).
2. **Wrong URL:** `DELETE /{table}/{id}.json` does not exist.
**Correct:**
```
GET /api/v2/{db_id}/{table}/delete.json?id={record_id}
```
Multiple deletes: `?id=1&id=2&id=3`. Also supports key-column: `?{KeyColumn}={value}`.
This is a confirmed design decision by TeamDesk — delete is a GET operation.
---
### 7. `select_query` — Fictional endpoint
**Code** (`server.py:784`):
```python
return await http_client.request("POST", token, "select.json", json_data={"query": arguments["query"]})
```
**Problem:** TeamDesk has **no SQL-like query endpoint**. There is no way to POST a query string to `select.json`. All queries use `GET` with `filter`, `column`, `sort`, `top`, `skip` parameters.
**Suggestion:** Remove this tool. The `get_records` tool already provides select functionality with filter/column/sort/top/skip parameters. If the intent is to give the AI a more natural query interface, the MCP tool descriptions should teach the AI how to compose filter expressions in TeamDesk formula syntax (e.g., `[Status]="Active" And [Amount]>1000`).
---
### 8. `select_from_view` — Reversed path segments
**Code** (`server.py:803`):
```python
return await http_client.request("GET", token, f"{view}/{table}/select.json", params=params)
```
**Problem:** Path segments are reversed. TeamDesk requires the table first, then the view.
**Correct:**
```
GET /api/v2/{db_id}/{table}/{view}/select.json?parameters
```
View names with spaces need URL encoding (e.g., `List%20All`).
---
### 9. `get_attachment_url` — Web UI URL, not API URL
**Code** (`server.py:810-811`):
```python
url = f"https://www.teamdesk.net/secure/db/{TEAMDESK_DATABASE_ID}/attachment.aspx?fid={arguments['field_id']}&guid={arguments['guid']}"
```
**Problem:** `attachment.aspx` is the TeamDesk **web interface**, not the REST API. This URL requires an active browser session and won't work for programmatic access.
**Correct API endpoint for attachment download:**
```
GET /api/v2/{db_id}/{table}/{column}/attachment?id={row_id}
GET /api/v2/{db_id}/{table}/{column}/attachment/{guid}
```
Both the table name and the attachment column name are required path segments. The response is the raw binary file with appropriate `Content-Type`, `Content-Disposition`, and metadata headers (`X-Author`, `X-Revision`, `ETag`, `Last-Modified`).
---
### 10. `get_records` sort parameter — Incorrect syntax
**Code** (`server.py:735-736`):
```python
if arguments.get("desc"):
params["desc"] = "true"
```
**Problem:** TeamDesk does not accept a `desc` query parameter. Sort direction is encoded in the `sort` parameter itself.
**Correct syntax:**
```
sort=ColumnName//ASC
sort=ColumnName//DESC
```
---
## Critical: Security Issues
### 11. Filter injection in API key validation
**Code** (`server.py:215`):
```python
filter_query = f"[Key]='{api_key}'"
```
The `api_key` value is interpolated directly into a TeamDesk filter expression with no escaping. A crafted API key like `' Or 1=1 Or '` could bypass validation by matching all records.
**Suggestion:** Validate that `api_key` matches a strict pattern (e.g., hex characters only, fixed length) before using it in a filter. TeamDesk filter expressions don't support parameterized queries, so input validation is the primary defense.
---
### 12. API key in query parameter
**Code** (`server.py:871`):
```python
api_key = request.headers.get("X-API-Key") or request.query_params.get("api_key")
```
Accepting API keys as query parameters means they appear in server access logs, browser history, Traefik logs, and any intermediary proxy logs. Header-only authentication is the safer pattern.
---
### 13. CORS wildcard in production
**Code** (`server.py:44`, `docker-compose.yml:31`):
```python
MCP_CORS_ORIGINS = os.getenv("MCP_CORS_ORIGINS", "*").split(",")
```
The default `*` and the docker-compose config both allow any origin. For a server that accepts bearer tokens and mutating operations, this should be restricted to known origins.
---
### 14. Hardcoded IP in wrapper
**Code** (`wrapper/teamdesk-mcp-wrapper.js:13`):
```javascript
const SERVER_URL = process.env.TEAMDESK_MCP_URL || 'http://72.61.219.5:8080';
```
A raw IP address over HTTP (no TLS) as the default server URL. This should use HTTPS and a proper hostname. The docker-compose.yml does configure Traefik with TLS for `mcp.forgreen.com.br` — the wrapper default should match.
---
### 15. `update_last_use` references nonexistent attribute and uses wrong HTTP method
**Code** (`server.py:947`, `server.py:282-286`):
```python
if validation.record_id: # record_id doesn't exist on ApiKeyValidationResult
...
await http_client.request(
method="PUT", # TeamDesk upsert.json requires POST, not PUT
...
)
```
Two issues:
- `ApiKeyValidationResult` has no `record_id` attribute (line 167-182), so `update_last_use` is never called — silently fails via `AttributeError`.
- Even if it were called, `PUT` to `upsert.json` would fail because TeamDesk requires `POST`.
---
## Major: MCP Protocol Issues
### 16. SSE transport doesn't execute tools
The MCP server registers a `call_tool` handler (`server.py:676-680`) that returns a static error:
```python
return [TextContent(type="text", text=json.dumps({"error": "Use o endpoint HTTP"}))]
```
The SSE endpoint (`/sse`) connects the MCP protocol transport but all tool calls through it will return this error. The HTTP `/tools/call` endpoint has its own separate execution path. This means the server doesn't work as a standard MCP server — only through the custom HTTP bridge.
For Claude Desktop integration, the standard approach is stdio transport (stdin/stdout JSON-RPC). The wrapper translates stdio to HTTP, adding an extra hop. Consider implementing tools directly in the `call_tool` handler so the SSE transport works, or dropping the SSE endpoint and using stdio directly.
---
### 17. Wrapper architecture adds complexity
The wrapper (`teamdesk-mcp-wrapper.js`) translates:
```
Claude Desktop ↔ stdio (JSON-RPC) ↔ wrapper ↔ HTTP ↔ server.py ↔ HTTP ↔ TeamDesk API
```
This is four network hops instead of one. A simpler architecture:
```
Claude Desktop ↔ stdio ↔ MCP server ↔ HTTP ↔ TeamDesk API
```
The wrapper exists because the Python server uses HTTP/SSE transport instead of stdio. Consider using `mcp.server.stdio` transport directly, which eliminates the need for the wrapper, the Starlette web framework, the HTTP client in the wrapper, and the deployment of a persistent server process.
---
## Moderate: Missing API Capabilities
### 18. No `retrieve.json` support
The `get_record` tool tries to fetch a single record but uses the wrong endpoint. Even with the URL fixed, the tool only accepts a single integer `record_id`. TeamDesk's `retrieve.json` supports:
- Multiple IDs: `?id=1&id=2&id=3`
- Key column lookup: `?{KeyColumn}={value}`
- Column projection: `?column=Name&column=Email`
A `retrieve` tool that exposes these capabilities would be more useful than a single-record-by-ID tool.
---
### 19. No pagination
The `get_records` tool caps results at 1000 (`server.py:729`):
```python
params["top"] = min(arguments["top"], 1000)
```
But provides no guidance to the AI about pagination. TeamDesk's default page size is 500. For large result sets, the AI needs to use `top` + `skip` iteratively. The tool description should mention this, and ideally an auto-paginating variant could be offered.
---
### 20. No delta sync support
TeamDesk provides `updated.json` and `deleted.json` endpoints for incremental data sync. These are missing from the MCP tools. They're essential for any workflow that needs to track changes since a last sync point.
---
### 21. No attachment download/upload
The `get_attachment_url` tool generates an unusable web UI URL. Actual attachment download requires the API endpoint, and upload uses `multipart/related` encoding with create/update/upsert. These are complex but important for any MCP integration that needs to work with file attachments.
---
## Minor Issues
### 22. Caching scope
Cache keys are derived from `token + operation + params` (`server.py:124-128`). If multiple API keys resolve to the same TeamDesk token (e.g., the master token fallback at line 259), they'll share cache entries. This is probably fine for `describe` results but worth being aware of.
---
### 23. No `workflow=0` support
TeamDesk supports `?workflow=0` on create/update/upsert to suppress automation triggers. This is important for bulk data operations. The tools don't expose this parameter.
---
### 24. Table name sanitization removes valid characters
**Code** (`server.py:411-412`):
```python
allowed = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_ -")
return "".join(c for c in name if c in allowed)[:100]
```
TeamDesk table names can include characters like parentheses, forward slashes, and other punctuation. This sanitizer would silently mangle such names. A better approach is to URL-encode the table name for the path segment rather than stripping characters.
---
### 25. Database ID hardcoded in docker-compose
**`docker-compose.yml:24`:**
```yaml
- TEAMDESK_DATABASE_ID=101885
```
This should be sourced from an `.env` file or secrets manager, not committed to source control.
---
### 26. Tests directory is empty
`tests/__init__.py` contains only a docstring. There are no tests for any of the URL construction, parameter handling, or error paths. Given the number of incorrect endpoints, automated tests would have caught most of these issues.
---
## Summary of Required Fixes by Tool
| Tool | Endpoint Fix | Method Fix | Body Fix | Notes |
|------|-------------|------------|----------|-------|
| `list_tables` | `tables.json` → `describe.json` | — | — | Parse `tables` from response object |
| `describe_table` | `{table}/columns.json` → `{table}/describe.json` | — | — | Response is object, not array |
| `get_records` | Correct | — | — | Fix sort syntax (remove `desc` param) |
| `get_record` | `{table}/{id}.json` → `{table}/retrieve.json?id={id}` | — | — | Consider supporting multiple IDs |
| `create_record` | `{table}.json` → `{table}/create.json` | — | Wrap `data` in array | Parse response for new ID |
| `update_record` | `{table}/{id}.json` → `{table}/update.json` | `PUT` → `POST` | Add `@row.id`, wrap in array | — |
| `delete_record` | `{table}/{id}.json` → `{table}/delete.json?id={id}` | `DELETE` → `GET` | Remove body | Yes, GET for delete |
| `select_query` | Remove entirely | — | — | No such endpoint exists |
| `upsert_records` | Correct | Correct | — | Working as-is |
| `select_from_view` | `{view}/{table}/` → `{table}/{view}/` | — | — | Reversed path segments |
| `get_attachment_url` | `attachment.aspx` → API endpoint | — | — | Needs table + column in path |
---
## Recommendations
1. **Fix all endpoints first.** The current server will not work against the TeamDesk API. Every tool except `upsert_records` and `get_records` (partially) constructs incorrect URLs.
2. **Write tests.** Create a simple test that validates each tool constructs the correct URL, uses the correct HTTP method, and formats the request body correctly. This can be done without hitting the live API by mocking `httpx`.
3. **Consider stdio transport.** Claude Desktop's native MCP transport is stdio. Using `mcp.server.stdio` eliminates the need for the wrapper, the web server, the HTTP bridge, the CORS configuration, and the deployment infrastructure. The entire server can be a single Python file.
5. **Fix the security issues.** At minimum: sanitize the filter injection in API key validation, remove query parameter API key support, restrict CORS origins, and use HTTPS by default in the wrapper.