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
60 changes: 55 additions & 5 deletions resources/blog/claude-code-cursor-database-mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Restart Cursor after editing the file. If TablePro is installed somewhere other

## Enable the server in TablePro {#turn-on}

1. Open **Settings > Integrations** and turn on **Enable MCP Server**. **Status** reads **Running on port 23508**, or the port it took instead when that one is busy.
1. Open **Settings > MCP** and turn on **Enable MCP Server**. **Status** reads **Running on port 23508**, or the port it took instead when that one is busy.
2. Click **Connect a Client…** and pick **Claude Code** or **Cursor**. The sheet shows the steps and a snippet with the path of your own install. TablePro does not edit other apps' config files, so paste it yourself.
3. Ask the client to list TablePro's tools, or to call `list_connections`. The list includes `list_connections`, `list_tables`, `describe_table` and `execute_query`.

Expand All @@ -56,7 +56,7 @@ Every saved connection has its own setting. Edit the connection, open the **Opti
| **Read Only** | Read the schema and run reads. Any write is refused before it reaches the database. This is the default. |
| **Read & Write** | Reads, and writes that pass Safe Mode. |

Leave production on **Read Only** or **Blocked**. Give **Read & Write** to a local or staging database where you want the agent to change data.
Leave production on **Read Only** or **Blocked**, and give the agent a [read-only database role](#database-role). Give **Read & Write** to a local or staging database where you want the agent to change data.

## Require approval for writes {#safe-mode}

Expand All @@ -83,18 +83,68 @@ With **Read & Write** and **Silent**, ordinary writes can run without a prompt.

Some statements are refused whatever the settings: anything that reads or writes files on the server or runs server-side code, and more than one statement in a single call, except a SQL Server script.

For a narrower token than the one `tablepro-mcp` carries, such as **Read Only** limited to a few connections, generate one under **Settings > Integrations > Authentication** and connect the client over HTTP instead. The [MCP Clients](https://docs.tablepro.app/integrations/mcp-clients#http-transport) docs show that config.
For a narrower token than the one `tablepro-mcp` carries, such as **Read Only** limited to a few connections, generate one under **Settings > MCP > Authentication** and connect the client over HTTP instead. The [MCP Clients](https://docs.tablepro.app/integrations/mcp-clients#http-transport) docs show that config.

## Use a read-only database role {#database-role}

Every check above runs in TablePro and judges a statement by its text. TablePro knows which built-in functions change data, such as `nextval` or `set_config`, but not what your own functions do: `SELECT archive_orders()` reads like a read. The database checks the role's privileges on every statement, whatever sent it.

For production, connect the agent as a role that can only read. On PostgreSQL, run the grants as the role that owns the tables, because `ALTER DEFAULT PRIVILEGES` covers only tables that role creates later:

```sql
CREATE ROLE agent LOGIN PASSWORD 'change-me';
GRANT CONNECT ON DATABASE app TO agent;
GRANT USAGE ON SCHEMA public TO agent;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO agent;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO agent;
```

On MySQL:

```sql
CREATE USER 'agent'@'%' IDENTIFIED BY 'change-me';
GRANT SELECT ON app.* TO 'agent'@'%';
```

Replace `app` with your database, and repeat the schema grants for each schema the agent should read. Then give the role its own connection:

1. On the welcome window, right-click the production connection and choose **Duplicate**.
2. Edit the copy to sign in as `agent`, and set its **External Clients** to **Read Only**.
3. Set **External Clients** on the original to **Blocked**, so clients see only the copy.

To check the role, open the copy in TablePro and run a write that matches no rows:

```sql
UPDATE orders SET status = status WHERE false;
```

PostgreSQL answers `permission denied for table orders`, MySQL `UPDATE command denied`. If it runs, the role can write.

What a read-only role does not cover:

- On PostgreSQL, a `SECURITY DEFINER` function runs with its owner's privileges, and any role may call a function unless `EXECUTE` is revoked from `PUBLIC`. List them, and revoke `EXECUTE` from `PUBLIC` on any that write:

```sql
SELECT p.oid::regprocedure
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE p.prosecdef AND n.nspname NOT IN ('pg_catalog', 'information_schema');
```

- On MySQL, grant nothing beyond `SELECT`. A routine runs as its definer by default, so `EXECUTE` would let the agent call one that writes.
- `ALTER ROLE agent SET default_transaction_read_only = on` is not a boundary: the session can `SET` it back.

## Review activity {#activity}

**View Activity…** in **Settings > Integrations** lists every tool call and query, with the connection and the outcome. Statements there are stored as a SHA-256 digest, not as text. Queries a client runs also appear in the query history of that connection, unless you turn off **Log MCP queries in history**.
**View Activity…** in **Settings > MCP** lists every tool call and query, with the connection and the outcome. Statements there are stored as a SHA-256 digest, not as text. Queries a client runs also appear in the query history of that connection, unless you turn off **Log MCP queries in history**.

## Limitations {#limits}

- The server listens on `127.0.0.1` only. There is no remote mode; a client on another machine needs an SSH port forward you set up and own.
- It runs in the Mac app. The iPhone and iPad app has no MCP server.
- Clients see your saved connections, never their passwords, and cannot create or edit connections or change Safe Mode.
- A result stops at **Default row limit**, 500 rows unless you change it, and a client can ask for more only up to **Maximum row limit**, 10,000 by default. Both are in **Settings > Integrations**.
- A result stops at **Default row limit**, 500 rows unless you change it, and a client can ask for more only up to **Maximum row limit**, 10,000 by default.
- A call waits at most **Query timeout**, 30 seconds unless you change it, and a client can ask for up to 300. A read still running then is cancelled. All three limits are in **Settings > MCP**.

## Troubleshooting {#troubleshooting}

Expand Down
60 changes: 55 additions & 5 deletions resources/blog/vi/claude-code-cursor-database-mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Khởi động lại Cursor sau khi sửa file. Nếu TablePro được cài ở

## Bật server trong TablePro {#turn-on}

1. Mở **Cài đặt > Tích hợp** (Settings > Integrations) và bật **Bật MCP Server** (Enable MCP Server). **Trạng thái** (Status) hiện **Đang chạy trên cổng 23508** (Running on port 23508), hoặc port mà server lấy thay khi port đó đang bận.
1. Mở **Cài đặt > MCP** (Settings > MCP) và bật **Bật MCP Server** (Enable MCP Server). **Trạng thái** (Status) hiện **Đang chạy trên cổng 23508** (Running on port 23508), hoặc port mà server lấy thay khi port đó đang bận.
2. Bấm **Kết nối client…** (Connect a Client…) và chọn **Claude Code** hoặc **Cursor**. Bảng này có các bước thiết lập và đoạn cấu hình chứa đúng đường dẫn bản cài của bạn. TablePro không sửa file cấu hình của ứng dụng khác, nên bạn tự dán vào.
3. Nhờ client liệt kê các tool của TablePro, hoặc gọi `list_connections`. Danh sách có `list_connections`, `list_tables`, `describe_table` và `execute_query`.

Expand All @@ -56,7 +56,7 @@ Mỗi connection đã lưu có thiết lập riêng. Sửa connection, mở tab
| **Read Only** | Đọc schema và chạy câu lệnh đọc. Mọi câu lệnh ghi bị từ chối trước khi tới cơ sở dữ liệu. Đây là mức mặc định. |
| **Read & Write** | Đọc, và ghi những gì Safe Mode cho qua. |

Hãy để production ở **Read Only** hoặc **Blocked**. Chỉ cho **Read & Write** với cơ sở dữ liệu local hoặc staging, nơi bạn muốn agent thay đổi dữ liệu.
Hãy để production ở **Read Only** hoặc **Blocked**, và cho agent dùng một [role chỉ đọc trong cơ sở dữ liệu](#database-role). Chỉ cho **Read & Write** với cơ sở dữ liệu local hoặc staging, nơi bạn muốn agent thay đổi dữ liệu.

## Yêu cầu duyệt trước khi ghi {#safe-mode}

Expand All @@ -83,18 +83,68 @@ Với **Read & Write** và **Silent**, thao tác ghi thông thường chạy đ

Một số câu lệnh luôn bị từ chối bất kể thiết lập: mọi thứ đọc hoặc ghi file trên server hay chạy code phía server, và nhiều hơn một câu lệnh trong một lần gọi, trừ script SQL Server.

Nếu cần token hẹp hơn token của `tablepro-mcp`, ví dụ **Read Only** và chỉ cho vài connection, hãy tạo một token trong **Cài đặt > Tích hợp > Xác thực** (Settings > Integrations > Authentication) rồi cho client kết nối qua HTTP. Tài liệu [MCP Clients](https://docs.tablepro.app/integrations/mcp-clients#http-transport) có cấu hình cho cách này.
Nếu cần token hẹp hơn token của `tablepro-mcp`, ví dụ **Read Only** và chỉ cho vài connection, hãy tạo một token trong **Cài đặt > MCP > Xác thực** (Settings > MCP > Authentication) rồi cho client kết nối qua HTTP. Tài liệu [MCP Clients](https://docs.tablepro.app/integrations/mcp-clients#http-transport) có cấu hình cho cách này.

## Dùng role chỉ đọc trong cơ sở dữ liệu {#database-role}

Mọi lớp kiểm tra ở trên chạy trong TablePro và đánh giá câu lệnh qua nội dung của nó. TablePro biết function có sẵn nào thay đổi dữ liệu, như `nextval` hay `set_config`, nhưng không biết function của chính bạn làm gì: `SELECT archive_orders()` trông như một câu lệnh đọc. Còn cơ sở dữ liệu kiểm tra quyền của role ở mọi câu lệnh, bất kể ai gửi.

Với production, hãy cho agent kết nối bằng một role chỉ đọc được. Trên PostgreSQL, chạy các lệnh grant bằng role sở hữu các table, vì `ALTER DEFAULT PRIVILEGES` chỉ áp dụng cho table mà chính role đó tạo về sau:

```sql
CREATE ROLE agent LOGIN PASSWORD 'change-me';
GRANT CONNECT ON DATABASE app TO agent;
GRANT USAGE ON SCHEMA public TO agent;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO agent;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO agent;
```

Trên MySQL:

```sql
CREATE USER 'agent'@'%' IDENTIFIED BY 'change-me';
GRANT SELECT ON app.* TO 'agent'@'%';
```

Thay `app` bằng tên cơ sở dữ liệu của bạn, và lặp lại các lệnh grant schema cho mỗi schema mà agent cần đọc. Sau đó tạo cho role này một connection riêng:

1. Trên cửa sổ chào mừng, bấm chuột phải vào connection production và chọn **Nhân bản** (Duplicate).
2. Sửa bản sao để đăng nhập bằng `agent`, và đặt **Client bên ngoài** (External Clients) của nó thành **Read Only**.
3. Đặt **Client bên ngoài** của connection gốc thành **Blocked**, để client chỉ thấy bản sao.

Để kiểm tra role, mở bản sao trong TablePro và chạy một câu lệnh ghi không khớp dòng nào:

```sql
UPDATE orders SET status = status WHERE false;
```

PostgreSQL trả về `permission denied for table orders`, MySQL trả về `UPDATE command denied`. Nếu câu lệnh chạy được, role đó vẫn ghi được.

Những gì role chỉ đọc không che được:

- Trên PostgreSQL, function `SECURITY DEFINER` chạy với quyền của owner, và role nào cũng gọi được function trừ khi `EXECUTE` đã bị thu hồi khỏi `PUBLIC`. Liệt kê các function đó, rồi thu hồi `EXECUTE` khỏi `PUBLIC` ở những function có ghi dữ liệu:

```sql
SELECT p.oid::regprocedure
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE p.prosecdef AND n.nspname NOT IN ('pg_catalog', 'information_schema');
```

- Trên MySQL, đừng cấp gì ngoài `SELECT`. Routine mặc định chạy với quyền của definer, nên `EXECUTE` sẽ cho agent gọi được một routine có ghi dữ liệu.
- `ALTER ROLE agent SET default_transaction_read_only = on` không phải ranh giới: session có thể `SET` lại.

## Xem hoạt động {#activity}

**Xem hoạt động…** (View Activity…) trong **Cài đặt > Tích hợp** liệt kê mọi lần gọi tool và mọi query, kèm connection và kết quả. Câu lệnh ở đó được lưu dưới dạng mã băm SHA-256, không phải văn bản. Query do client chạy cũng xuất hiện trong lịch sử query của connection đó, trừ khi bạn tắt **Ghi truy vấn MCP vào lịch sử** (Log MCP queries in history).
**Xem hoạt động…** (View Activity…) trong **Cài đặt > MCP** liệt kê mọi lần gọi tool và mọi query, kèm connection và kết quả. Câu lệnh ở đó được lưu dưới dạng mã băm SHA-256, không phải văn bản. Query do client chạy cũng xuất hiện trong lịch sử query của connection đó, trừ khi bạn tắt **Ghi truy vấn MCP vào lịch sử** (Log MCP queries in history).

## Giới hạn {#limits}

- Server chỉ lắng nghe trên `127.0.0.1`. Không có chế độ truy cập từ xa; client trên máy khác cần một SSH port forward do bạn tự thiết lập và tự chịu trách nhiệm.
- Server chạy trong ứng dụng cho Mac. Ứng dụng cho iPhone và iPad không có MCP server.
- Client thấy các connection đã lưu nhưng không bao giờ thấy mật khẩu, và không tạo, sửa connection hay đổi Safe Mode được.
- Kết quả dừng ở **Giới hạn dòng mặc định** (Default row limit), 500 dòng nếu bạn không đổi, và client chỉ xin thêm được tới **Giới hạn dòng tối đa** (Maximum row limit), mặc định là 10.000. Cả hai nằm trong **Cài đặt > Tích hợp**.
- Kết quả dừng ở **Giới hạn dòng mặc định** (Default row limit), 500 dòng nếu bạn không đổi, và client chỉ xin thêm được tới **Giới hạn dòng tối đa** (Maximum row limit), mặc định là 10.000.
- Mỗi lần gọi chờ tối đa **Thời gian chờ truy vấn** (Query timeout), 30 giây nếu bạn không đổi, và client xin được tới 300 giây. Câu lệnh đọc còn chạy lúc đó sẽ bị hủy. Cả ba giới hạn nằm trong **Cài đặt > MCP** (Settings > MCP).

## Xử lý lỗi {#troubleshooting}

Expand Down
9 changes: 9 additions & 0 deletions tests/Feature/Landing/BlogTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,15 @@ function blogSlotIds(string $html): array
expect(glob(resource_path('blog/vi/*.md')))->toHaveCount(count(BLOG_GUIDES));
});

it('gives each Vietnamese guide the same sections as the original, in the same order', function (): void {
$anchors = fn(string $path): array => preg_match_all('/^#{2,6} .*\{#([\w-]+)\}\s*$/m', File::get($path), $matches) > 0 ? $matches[1] : [];

foreach (array_keys(BLOG_GUIDES) as $slug) {
expect($anchors(resource_path("blog/vi/{$slug}.md")))
->toBe($anchors(resource_path("blog/{$slug}.md")), "{$slug}: a section of the original is missing or out of place in the translation");
}
});

it('removes the merged guides, whose URLs now redirect', function (string $slug, string $target): void {
expect(File::exists(resource_path("blog/{$slug}.md")))->toBeFalse();

Expand Down