In this post we are going to cover how to connect Claude Code to a PostgreSQL database with MCP, the safe way, with a read-only login. We create the read-only role on a small shop database, prove that it cannot change data, test the MCP server on its own, add it to Claude Code with one command, and ask questions about the real data. The last step is a look at connectors, the same idea in the Claude app.
Out of the box, Claude Code sees your files, your terminal and your git history. It does not see your database. You paste the schema, or it runs psql for you. MCP, the Model Context Protocol, is a way to connect Claude to outside tools and data. An MCP server offers Claude a set of tools, and Claude calls them when it needs them. For a data engineer that means Claude can look up the tables and run a query itself, and answer from the real rows.
Part 6 of the Claude for Data Engineers series on TechBrothersIT.
Let's create some sample data
Our project is shop-analytics, with a PostgreSQL 16 database called shop_analytics. It has three tables: 8 customers, 14 orders from September to November 2026, and 15 payments. The project also has a small test client, tools/mcp_call.py, that we use in step 2, and a CLAUDE.md that says my own login is an admin login, only for setup and never for the MCP server. Here are the starting files and the commands that create and load the database.
CREATE TABLE customers (
customer_id int PRIMARY KEY,
first_name text NOT NULL,
last_name text NOT NULL,
email text NOT NULL,
country text NOT NULL
);
CREATE TABLE orders (
order_id int PRIMARY KEY,
customer_id int NOT NULL REFERENCES customers,
order_date date NOT NULL,
status text NOT NULL
);
CREATE TABLE payments (
payment_id int PRIMARY KEY,
order_id int NOT NULL REFERENCES orders,
method text NOT NULL,
amount numeric(10, 2) NOT NULL
);INSERT INTO customers VALUES
(1, 'Sara', 'Khan', 'sara.khan@example.com', 'US'),
(2, 'Omar', 'Ali', 'omar.ali@example.com', 'UK'),
(3, 'Maya', 'Chen', 'maya.chen@example.com', 'US'),
(4, 'Liam', 'Brooks', 'liam.brooks@example.com', 'US'),
(5, 'Ana', 'Silva', 'ana.silva@example.com', 'BR'),
(6, 'Noah', 'Weber', 'noah.weber@example.com', 'DE'),
(7, 'Zara', 'Ahmed', 'zara.ahmed@example.com', 'UK'),
(8, 'Ethan', 'Park', 'ethan.park@example.com', 'US');
INSERT INTO orders VALUES
(1001, 1, '2026-09-04', 'completed'),
(1002, 3, '2026-09-12', 'completed'),
(1003, 2, '2026-09-20', 'shipped'),
(1004, 5, '2026-09-28', 'completed'),
(1005, 1, '2026-10-02', 'completed'),
(1006, 3, '2026-10-05', 'completed'),
(1007, 4, '2026-10-07', 'returned'),
(1008, 6, '2026-10-11', 'completed'),
(1009, 7, '2026-10-14', 'shipped'),
(1010, 3, '2026-10-18', 'completed'),
(1011, 8, '2026-10-21', 'completed'),
(1012, 2, '2026-10-25', 'placed'),
(1013, 6, '2026-10-29', 'shipped'),
(1014, 1, '2026-11-01', 'placed');
INSERT INTO payments VALUES
(1, 1001, 'card', 129.00),
(2, 1002, 'paypal', 45.50),
(3, 1003, 'card', 89.99),
(4, 1004, 'card', 19.99),
(5, 1005, 'card', 74.50),
(6, 1005, 'gift_card', 25.00),
(7, 1006, 'card', 158.00),
(8, 1007, 'card', 32.00),
(9, 1008, 'bank_transfer', 64.00),
(10, 1009, 'card', 22.75),
(11, 1010, 'paypal', 81.20),
(12, 1011, 'paypal', 99.00),
(13, 1012, 'card', 44.10),
(14, 1013, 'card', 51.30),
(15, 1014, 'card', 60.00);"""Test an MCP server before you add it to Claude Code.
Starts the server over stdio (the same command you give `claude mcp add`), then calls one tool.
python tools/mcp_call.py tools list the server's tools
python tools/mcp_call.py search_objects object_type=table call a tool with arguments
python tools/mcp_call.py execute_sql sql="SELECT count(*) FROM orders"
"""
import json, subprocess, sys
DSN = "postgresql://mcp_readonly:change-me@localhost:5432/shop_analytics"
SERVER = ["npx", "-y", "@bytebase/dbhub", "--dsn", DSN]
proc = subprocess.Popen(SERVER, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
next_id = 0
def send(method, params=None):
"""One JSON-RPC request: MCP over stdio is one JSON message per line."""
global next_id
next_id += 1
proc.stdin.write(json.dumps({"jsonrpc": "2.0", "id": next_id, "method": method, "params": params or {}}) + "\n")
proc.stdin.flush()
for line in proc.stdout:
msg = json.loads(line)
if msg.get("id") == next_id:
return msg
raise SystemExit("the server closed the connection")
send("initialize", {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "mcp_call", "version": "1.0"}})
proc.stdin.write(json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n")
proc.stdin.flush()
if sys.argv[1] == "tools":
for tool in send("tools/list")["result"]["tools"]:
print(f"{tool['name']:<16} {tool['description']}")
code = 0
else:
args = dict(a.split("=", 1) for a in sys.argv[2:])
result = send("tools/call", {"name": sys.argv[1], "arguments": args})["result"]
print(result["content"][0]["text"]) # exactly what the server sends back to Claude
code = 1 if result.get("isError") else 0
proc.terminate()
sys.exit(code)# shop-analytics
Analytics work on the online shop database.
- PostgreSQL 16 on localhost, database `shop_analytics` (tables in db/schema.sql)
- My own login (PGUSER) is an admin login: only for setup, never for the MCP server
- tools/mcp_call.py tests an MCP server before it is added to Claude Codepsql -d postgres -q -c "DROP DATABASE IF EXISTS shop_analytics WITH (FORCE)" -c "DROP ROLE IF EXISTS mcp_readonly" -c "CREATE DATABASE shop_analytics" 2>/dev/null
psql -q -v ON_ERROR_STOP=1 -f db/schema.sql -f db/seed.sqlWhat MCP is
Let's put the two ways side by side.
Claude and your database
- Claude sees files, terminal, git
- You paste the schema and sample rows
- Or Claude runs psql through Bash
- The server offers Claude tools
- DBHub: execute_sql, search_objects
- Local server over stdio; remote over HTTP
Without MCP, Claude works from what you give it, or from psql through its Bash tool. With an MCP server, the server offers tools, and Claude calls them. For Postgres, the Claude Code docs use a server called DBHub. It runs on your machine as a local process and talks to Claude over standard input and output, which is called stdio. Remote servers talk over HTTP instead. DBHub gives Claude two tools: execute_sql and search_objects.
There is one warning in the docs that every data team should read.
Verify you trust each server before connecting it. Servers that fetch external content can expose you to prompt injection.
So only connect servers you trust, and give them only the access they need. That is what the next step is about.
Step 1: A read-only Postgres role
Before Claude touches the database, we give the server a login that can only read. We press Shift+Tab to switch to manual mode, so Claude asks before it writes a file or runs a command. That matters most when a database is involved.
Create a read-only Postgres role for the MCP server: SELECT on the public schema only. Put it in readonly_role.sql and run it.Claude says it will write the role in readonly_role.sql: login, connect, USAGE on public and SELECT on its tables, with no write grants. It asks to create the file, and we choose Yes. Then it wants to run the file with psql. This runs as my own admin login, so I read the command before I choose Yes.
-- A read-only login for the MCP server: it can read every table in public, and change nothing.
CREATE ROLE mcp_readonly LOGIN PASSWORD 'change-me';
GRANT CONNECT ON DATABASE shop_analytics TO mcp_readonly;
GRANT USAGE ON SCHEMA public TO mcp_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO mcp_readonly;psql -v ON_ERROR_STOP=1 -f readonly_role.sqlCREATE ROLE
GRANT
GRANT
GRANT
ALTER DEFAULT PRIVILEGESPostgres answers CREATE ROLE, three GRANT lines and ALTER DEFAULT PRIVILEGES. There is no insert, update or delete grant anywhere. The last line in the file makes new tables that this admin login creates later readable too.
Now Claude tests the new role, with the same connection string the MCP server will use. It asks before each command, and we choose Yes both times.
psql "postgresql://mcp_readonly:change-me@localhost:5432/shop_analytics" -c "SELECT count(*) AS orders FROM orders" orders
--------
14
(1 row)psql "postgresql://mcp_readonly:change-me@localhost:5432/shop_analytics" -c "DELETE FROM orders WHERE order_id = 1001"ERROR: permission denied for table ordersCounting the orders works: 14 orders. The DELETE fails with ERROR: permission denied for table orders, and psql exits with code 1. That is Postgres itself saying no, not Claude. Claude sums it up: mcp_readonly can read, Postgres refuses writes, so it is safe to give this login to the MCP server.
Never point this at production for a test. Use a dev copy, and grant only the schemas the role really needs.
Step 2: Test the MCP server first
Next, a habit that saves a lot of time: test the MCP server on its own, before you add it to Claude Code. Our test client tools/mcp_call.py starts DBHub with npx, with the same command and the same read-only connection string we will give Claude Code. Then it does the MCP handshake and calls one tool, and prints exactly what the server sends back.
Test the DBHub server with tools/mcp_call.py: list its tools, then try a DELETE through execute_sqlClaude wants to run the script, and we choose Yes, and don't ask again for this script, so the second call runs without a prompt.
python tools/mcp_call.py toolsexecute_sql Execute SQL queries on the postgres database
search_objects Search and list database objects on the postgres databaseThe server offers two tools: execute_sql and search_objects. Now a write, through the server this time.
python tools/mcp_call.py execute_sql sql="DELETE FROM orders WHERE order_id = 1001"{
"success": false,
"error": "permission denied for table orders",
"code": "EXECUTION_ERROR"
}The server passes the DELETE to Postgres, and Postgres refuses it. The response is "success": false with "error": "permission denied for table orders" and the code EXECUTION_ERROR. This is the exact text Claude would see.
So we have two locks. Claude asks before it acts, and the login itself cannot write. DBHub also has its own read-only mode and row limits, which are good extras on top.
If a server does not work in Claude Code, you cannot tell if the problem is the server, the connection string or Claude Code. Testing the server alone first takes that guess away.
Step 3: Add the server with claude mcp add
Now we add the server to Claude Code. This is one command, in your own terminal.
claude mcp add
# Add DBHub to Claude Code (local scope: this project, only you)
claude mcp add --transport stdio shopdb -- npx -y @bytebase/dbhub \
--dsn "postgresql://mcp_readonly:change-me@localhost:5432/shop_analytics"
# Shared with the team instead: --scope project writes .mcp.json in the repo
claude mcp add --scope project --transport stdio shopdb -- npx -y @bytebase/dbhub \
--dsn '${SHOPDB_DSN}' # single quotes: .mcp.json keeps ${SHOPDB_DSN}, set it in your shell
# Check and manage
claude mcp list
claude mcp get shopdb
claude mcp remove shopdb
/mcp # inside a session: status of each serverFirst claude mcp add, then --transport stdio and a name, shopdb. Everything after the two dashes is the command that starts the server: the same npx command and the same read-only connection string we just tested.
With no scope flag, the server is local. It is saved in ~/.claude.json in your home folder, for this project and only for you, so the password stays out of git. If your team should share the server, use --scope project. That writes .mcp.json in the repo, so keep the connection string in an environment variable. Note the single quotes around '${SHOPDB_DSN}': with double quotes your shell would replace the variable with the real value, and the password would end up in the file.
claude mcp list and claude mcp get shopdb show your servers, and claude mcp remove shopdb deletes one. Inside a session, /mcp shows the status of each server.
Step 4: Ask questions about real data
Time to use it. We start a new Claude Code session in the project, so it loads the new server. The first question is simple.
What tables are in the shop database?Claude says it will look it up with the shopdb server's search_objects tool. The call goes through MCP, not through Bash, so there is no command to print. To show the real response, we make the exact same tool call with our test client. This is the text the server returns to Claude.
python tools/mcp_call.py search_objects object_type=table{
"success": true,
"data": {
"object_type": "table",
"pattern": "%",
"detail_level": "names",
"count": 3,
"results": [
{
"name": "customers",
"schema": "public"
},
{
"name": "orders",
"schema": "public"
},
{
"name": "payments",
"schema": "public"
}
],
"truncated": false
}
}Three tables in public: customers, orders and payments, with "count": 3. Claude answers with the same three names. Now a real business question.
Who were our top 3 customers in October by amount paid? Leave out returned orders.Claude writes the SQL and sends it with execute_sql. The server runs it as mcp_readonly and returns the rows as JSON. Again, here is the same call through the test client.
python tools/mcp_call.py execute_sql sql="SELECT c.first_name || ' ' || c.last_name AS customer, count(DISTINCT o.order_id) AS orders, sum(p.amount) AS paid FROM orders o JOIN customers c USING (customer_id) JOIN payments p USING (order_id) WHERE o.order_date >= '2026-10-01' AND o.order_date < '2026-11-01' AND o.status <> 'returned' GROUP BY 1 ORDER BY paid DESC LIMIT 3"{
"success": true,
"data": {
"statements": [
{
"sql": "SELECT c.first_name || ' ' || c.last_name AS customer, count(DISTINCT o.order_id) AS orders, sum(p.amount) AS paid FROM orders o JOIN customers c USING (customer_id) JOIN payments p USING (order_id) WHERE o.order_date >= '2026-10-01' AND o.order_date < '2026-11-01' AND o.status <> 'returned' GROUP BY 1 ORDER BY paid DESC LIMIT 3",
"rows": [
{
"customer": "Maya Chen",
"orders": "2",
"paid": "239.20"
},
{
"customer": "Noah Weber",
"orders": "2",
"paid": "115.30"
},
{
"customer": "Sara Khan",
"orders": "1",
"paid": "99.50"
}
],
"count": 3
}
],
"source_id": "default"
}
}Maya Chen is first, with 2 orders and 239.20, then Noah Weber with 2 orders and 115.30, and Sara Khan with 1 order and 99.50. Sara's order has two payments, and count(DISTINCT o.order_id) makes sure it still counts as one order. Claude's answer lists the same three customers and the same numbers.
And we never pasted the schema. Claude looked it up through the server, and every number in the answer comes from the real result.
One last check, run with my own login outside Claude: after both DELETE attempts, order 1001 is still there.
psql -c "SELECT order_id, status FROM orders WHERE order_id = 1001" order_id | status
----------+-----------
1001 | completed
(1 row)The first time you use a new server, check one answer by hand with a query you run yourself. After that you know what the server returns and how Claude reads it.
Safe MCP for data teams
Here are the rules I follow for MCP on real data.
Five rules I follow
- A read-only role, SELECT only, on a dev copy
- Test the server first, then
claude mcp add - Secrets out of git: local scope, or
${VAR}in.mcp.json - Pre-allow read tools:
mcp__shopdb__search_objects - Output warning above 10,000 tokens, cap 25,000 (
MAX_MCP_OUTPUT_TOKENS)
A read-only role with SELECT only, on a dev copy of the data. Test the server on its own first, then add it with claude mcp add. Keep secrets out of git: local scope does that for you, and a shared .mcp.json can read them from environment variables. You can pre-allow the tools you trust in your permission rules. The rule is mcp, then the server name, then the tool name, joined by double underscores, like mcp__shopdb__search_objects. And watch result sizes: Claude Code warns above 10,000 tokens, and limits MCP output to 25,000 tokens by default.
Step 5: Connectors in the Claude app
Last, what about the Claude app? In the app, an MCP server is called a connector. You add one under Customize, then Connectors, then + Add and Add custom connector.
A name and the URL of a remote MCP server. It is reached from Anthropic’s cloud, so it must be publicly reachable.
You give it a name and the URL of a remote MCP server. Remote connectors run from Anthropic's cloud, so the server must be reachable from the internet. That means the local DBHub we used here is a Claude Code setup. For the app, you need a remote server with proper authentication in front of it.
Once a connector is added, you turn it on for a chat from the + button in the message box, under Connectors. And if you sign in to Claude Code with your Claude subscription, not an API key, connectors you add in the app also show up in /mcp.
Custom connectors work on every plan, but the Free plan is limited to one.
Key points to remember
- What MCP is (and DBHub)
- A read-only Postgres role
- Writes refused by Postgres, for real
- Test the server before adding it
- claude mcp add and scopes
- Real answers from execute_sql
- Connectors in the Claude app
The full script
This bash script writes every file Claude wrote, in the same order, and runs every command, so you can reproduce all the results without Claude.
Show the full script (every file and every command, in order)
#!/usr/bin/env bash
# Claude for Data Engineers #6: Connect Claude to Your Database with MCP
# Rebuilds the demo project without Claude: writes every file Claude wrote, in the same order,
# and runs every command from the video. Run it in an empty folder.
# Tools used in the video: Python 3.13, DuckDB 2.0.0, pandas 3.0.5, PySpark 4.3.0 (local mode, Java 21),
# dbt-core 1.12.6 with dbt-duckdb 1.11.0, PostgreSQL 16. Install the ones this project uses first.
# PostgreSQL: set PGHOST, PGUSER and PGDATABASE so psql connects to an EMPTY dev database first.
mkdir -p shop-analytics && cd shop-analytics
export PGDATABASE=shop_analytics
# ---------- starting files ----------
mkdir -p db
cat > db/schema.sql <<'CLAUDE_DE_EOF'
CREATE TABLE customers (
customer_id int PRIMARY KEY,
first_name text NOT NULL,
last_name text NOT NULL,
email text NOT NULL,
country text NOT NULL
);
CREATE TABLE orders (
order_id int PRIMARY KEY,
customer_id int NOT NULL REFERENCES customers,
order_date date NOT NULL,
status text NOT NULL
);
CREATE TABLE payments (
payment_id int PRIMARY KEY,
order_id int NOT NULL REFERENCES orders,
method text NOT NULL,
amount numeric(10, 2) NOT NULL
);
CLAUDE_DE_EOF
mkdir -p db
cat > db/seed.sql <<'CLAUDE_DE_EOF'
INSERT INTO customers VALUES
(1, 'Sara', 'Khan', 'sara.khan@example.com', 'US'),
(2, 'Omar', 'Ali', 'omar.ali@example.com', 'UK'),
(3, 'Maya', 'Chen', 'maya.chen@example.com', 'US'),
(4, 'Liam', 'Brooks', 'liam.brooks@example.com', 'US'),
(5, 'Ana', 'Silva', 'ana.silva@example.com', 'BR'),
(6, 'Noah', 'Weber', 'noah.weber@example.com', 'DE'),
(7, 'Zara', 'Ahmed', 'zara.ahmed@example.com', 'UK'),
(8, 'Ethan', 'Park', 'ethan.park@example.com', 'US');
INSERT INTO orders VALUES
(1001, 1, '2026-09-04', 'completed'),
(1002, 3, '2026-09-12', 'completed'),
(1003, 2, '2026-09-20', 'shipped'),
(1004, 5, '2026-09-28', 'completed'),
(1005, 1, '2026-10-02', 'completed'),
(1006, 3, '2026-10-05', 'completed'),
(1007, 4, '2026-10-07', 'returned'),
(1008, 6, '2026-10-11', 'completed'),
(1009, 7, '2026-10-14', 'shipped'),
(1010, 3, '2026-10-18', 'completed'),
(1011, 8, '2026-10-21', 'completed'),
(1012, 2, '2026-10-25', 'placed'),
(1013, 6, '2026-10-29', 'shipped'),
(1014, 1, '2026-11-01', 'placed');
INSERT INTO payments VALUES
(1, 1001, 'card', 129.00),
(2, 1002, 'paypal', 45.50),
(3, 1003, 'card', 89.99),
(4, 1004, 'card', 19.99),
(5, 1005, 'card', 74.50),
(6, 1005, 'gift_card', 25.00),
(7, 1006, 'card', 158.00),
(8, 1007, 'card', 32.00),
(9, 1008, 'bank_transfer', 64.00),
(10, 1009, 'card', 22.75),
(11, 1010, 'paypal', 81.20),
(12, 1011, 'paypal', 99.00),
(13, 1012, 'card', 44.10),
(14, 1013, 'card', 51.30),
(15, 1014, 'card', 60.00);
CLAUDE_DE_EOF
mkdir -p tools
cat > tools/mcp_call.py <<'CLAUDE_DE_EOF'
""Test an MCP server before you add it to Claude Code.
Starts the server over stdio (the same command you give `claude mcp add`), then calls one tool.
python tools/mcp_call.py tools list the servers tools
python tools/mcp_call.py search_objects object_type=table call a tool with arguments
python tools/mcp_call.py execute_sql sql="SELECT count(*) FROM orders"
""
import json, subprocess, sys
DSN = "postgresql://mcp_readonly:change-me@localhost:5432/shop_analytics"
SERVER = ["npx", "-y", "@bytebase/dbhub", "--dsn", DSN]
proc = subprocess.Popen(SERVER, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
next_id = 0
def send(method, params=None):
"""One JSON-RPC request: MCP over stdio is one JSON message per line."""
global next_id
next_id += 1
proc.stdin.write(json.dumps({"jsonrpc": "2.0", "id": next_id, "method": method, "params": params or {}}) + "\n")
proc.stdin.flush()
for line in proc.stdout:
msg = json.loads(line)
if msg.get("id") == next_id:
return msg
raise SystemExit("the server closed the connection")
send("initialize", {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "mcp_call", "version": "1.0"}})
proc.stdin.write(json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n")
proc.stdin.flush()
if sys.argv[1] == "tools":
for tool in send("tools/list")["result"]["tools"]:
print(f"{tool['name']:<16} {tool['description']}")
code = 0
else:
args = dict(a.split("=", 1) for a in sys.argv[2:])
result = send("tools/call", {"name": sys.argv[1], "arguments": args})["result"]
print(result["content"][0]["text"]) # exactly what the server sends back to Claude
code = 1 if result.get("isError") else 0
proc.terminate()
sys.exit(code)
CLAUDE_DE_EOF
cat > CLAUDE.md <<'CLAUDE_DE_EOF'
# shop-analytics
Analytics work on the online shop database.
- PostgreSQL 16 on localhost, database `shop_analytics` (tables in db/schema.sql)
- My own login (PGUSER) is an admin login: only for setup, never for the MCP server
- tools/mcp_call.py tests an MCP server before it is added to Claude Code
CLAUDE_DE_EOF
# ---------- setup ----------
psql -d postgres -q -c "DROP DATABASE IF EXISTS shop_analytics WITH (FORCE)" -c "DROP ROLE IF EXISTS mcp_readonly" -c "CREATE DATABASE shop_analytics" 2>/dev/null
psql -q -v ON_ERROR_STOP=1 -f db/schema.sql -f db/seed.sql
# ---------- step 1: Create a read-only role for the MCP server (run as your admin login) ----------
cat > readonly_role.sql <<'CLAUDE_DE_EOF'
-- A read-only login for the MCP server: it can read every table in public, and change nothing.
CREATE ROLE mcp_readonly LOGIN PASSWORD 'change-me';
GRANT CONNECT ON DATABASE shop_analytics TO mcp_readonly;
GRANT USAGE ON SCHEMA public TO mcp_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO mcp_readonly;
CLAUDE_DE_EOF
psql -v ON_ERROR_STOP=1 -f readonly_role.sql
# ---------- step 2: Prove the role can read but not write ----------
psql "postgresql://mcp_readonly:change-me@localhost:5432/shop_analytics" -c "SELECT count(*) AS orders FROM orders"
psql "postgresql://mcp_readonly:change-me@localhost:5432/shop_analytics" -c "DELETE FROM orders WHERE order_id = 1001" # expected to fail
# ---------- step 3: Start DBHub and list its tools ----------
python tools/mcp_call.py tools
# ---------- step 4: Try a DELETE through the MCP server (the database refuses it) ----------
python tools/mcp_call.py execute_sql sql="DELETE FROM orders WHERE order_id = 1001" # expected to fail
# ---------- step 5: What Claude gets back from search_objects ----------
python tools/mcp_call.py search_objects object_type=table
# ---------- step 6: What Claude gets back from execute_sql: top 3 customers in October ----------
python tools/mcp_call.py execute_sql sql="SELECT c.first_name || ' ' || c.last_name AS customer, count(DISTINCT o.order_id) AS orders, sum(p.amount) AS paid FROM orders o JOIN customers c USING (customer_id) JOIN payments p USING (order_id) WHERE o.order_date >= '2026-10-01' AND o.order_date < '2026-11-01' AND o.status <> 'returned' GROUP BY 1 ORDER BY paid DESC LIMIT 3"
# ---------- step 7: Order 1001 is still there ----------
psql -c "SELECT order_id, status FROM orders WHERE order_id = 1001"
Part 7: Claude Skills for Data Work: Build a Data-Quality Skill
The project and its data are invented for teaching. Every file on this page is the real file from the demo project, and every output is the real output of the command above it, captured when the video was made (each step ran twice in a fresh folder and the outputs were compared). The Claude screens in the video are a simplified recreation for teaching. Always review what Claude changes and runs. Claude plans, features and limits are current as of November 2026; check the official Claude and Claude Code documentation before you rely on them. Part of the Claude for Data Engineers series on TechBrothersIT.



No comments:
Post a Comment
Note: Only a member of this blog may post a comment.