Skip to content

Commit 8031f7c

Browse files
committed
CrateDB: Hello, World!
1 parent 1429b54 commit 8031f7c

File tree

3 files changed

+67
-91
lines changed

3 files changed

+67
-91
lines changed

README-CRATEDB.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# CrateDB Model Context Protocol Server
2+
3+
## About
4+
5+
This CrateDB MCP Server is based on the PostgreSQL Model Context Protocol (PG-MCP) Server.
6+
[pg-mcp] uses [asyncpg], so it can also be used with CrateDB.
7+
8+
`server/resources/schema.py` received a few adjustments to compensate for
9+
missing metadata features of CrateDB, nothing serious.
10+
11+
## Usage
12+
13+
Start CrateDB.
14+
```shell
15+
docker run --rm \
16+
--name=cratedb --publish=4200:4200 --publish=5432:5432 \
17+
--env=CRATE_HEAP_SIZE=2g crate/crate:nightly \
18+
-Cdiscovery.type=single-node
19+
```
20+
21+
Initialize Python environment.
22+
```shell
23+
git clone https://github.com/crate-workbench/pg-mcp --branch=cratedb
24+
cd pg-mcp
25+
uv venv --python 3.13 --seed .venv
26+
uv sync --frozen
27+
```
28+
29+
Run MCP server and test program.
30+
```shell
31+
uv run -m server.app
32+
uv run test.py "postgresql://crate@localhost/doc"
33+
```
34+
35+
Run example Claude session (untested).
36+
```shell
37+
export DATABASE_URL=postgresql://crate@localhost
38+
export ANTHROPIC_API_KEY=...
39+
uv run -m client.claude_cli "Give me 5 Austria mountains (querying specific tables, like sys.summits)"
40+
```
41+
42+
43+
[asyncpg]: https://pypi.org/project/asyncpg
44+
[pg-mcp]: https://github.com/stuzero/pg-mcp

server/resources/schema.py

Lines changed: 22 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ async def db_info(conn_id: str):
1818
# Get all non-system schemas
1919
schemas_query = """
2020
SELECT
21-
schema_name,
22-
obj_description(pg_namespace.oid) as description
21+
schema_name
2322
FROM information_schema.schemata
2423
JOIN pg_namespace ON pg_namespace.nspname = schema_name
2524
WHERE
@@ -45,9 +44,7 @@ async def db_info(conn_id: str):
4544
# Get all tables in the schema
4645
tables_query = """
4746
SELECT
48-
t.table_name,
49-
obj_description(format('"%s"."%s"', t.table_schema, t.table_name)::regclass::oid) as description,
50-
pg_stat_get_tuples_inserted(format('"%s"."%s"', t.table_schema, t.table_name)::regclass::oid) as row_count
47+
t.table_name
5148
FROM information_schema.tables t
5249
WHERE
5350
t.table_schema = $1
@@ -76,16 +73,15 @@ async def db_info(conn_id: str):
7673
c.column_name,
7774
c.data_type,
7875
c.is_nullable,
79-
c.column_default,
80-
col_description(format('"%s"."%s"', c.table_schema, c.table_name)::regclass::oid, c.ordinal_position) as description
76+
c.column_default
8177
FROM information_schema.columns c
8278
WHERE
8379
c.table_schema = $1 AND
8480
c.table_name = $2
8581
ORDER BY c.ordinal_position
8682
"""
8783
columns = await execute_query(columns_query, conn_id, [schema_name, table_name])
88-
84+
8985
# Get constraints for this table to identify primary keys, etc.
9086
constraints_query = """
9187
SELECT
@@ -97,18 +93,15 @@ async def db_info(conn_id: str):
9793
WHEN c.contype = 'f' THEN 'FOREIGN KEY'
9894
WHEN c.contype = 'c' THEN 'CHECK'
9995
ELSE 'OTHER'
100-
END as constraint_type_desc,
101-
ARRAY_AGG(col.attname ORDER BY u.attposition) as column_names
96+
END as constraint_type_desc
10297
FROM
10398
pg_constraint c
10499
JOIN
105100
pg_namespace n ON n.oid = c.connamespace
106101
JOIN
107102
pg_class t ON t.oid = c.conrelid
108103
LEFT JOIN
109-
LATERAL unnest(c.conkey) WITH ORDINALITY AS u(attnum, attposition) ON TRUE
110-
LEFT JOIN
111-
pg_attribute col ON col.attrelid = t.oid AND col.attnum = u.attnum
104+
pg_attribute col ON col.attrelid = t.oid
112105
WHERE
113106
n.nspname = $1
114107
AND t.relname = $2
@@ -118,7 +111,7 @@ async def db_info(conn_id: str):
118111
c.contype, c.conname
119112
"""
120113
constraints = await execute_query(constraints_query, conn_id, [schema_name, table_name])
121-
114+
122115
# Process columns and add constraint information
123116
for column in columns:
124117
column_name = column['column_name']
@@ -128,56 +121,22 @@ async def db_info(conn_id: str):
128121
for constraint in constraints:
129122
if column_name in constraint.get('column_names', []):
130123
column_constraints.append(constraint['constraint_type_desc'])
131-
124+
132125
# Add column info
133126
column_info = {
134127
"name": column_name,
135128
"type": column['data_type'],
136129
"nullable": column['is_nullable'] == 'YES',
137130
"default": column['column_default'],
138-
"description": column['description'],
139131
"constraints": column_constraints
140132
}
141133

142134
table_info["columns"].append(column_info)
143135

144136
# Process foreign key constraints
145-
foreign_keys_query = """
146-
SELECT
147-
c.conname as constraint_name,
148-
ARRAY_AGG(col.attname ORDER BY u.attposition) as column_names,
149-
nr.nspname as referenced_schema,
150-
ref_table.relname as referenced_table,
151-
ARRAY_AGG(ref_col.attname ORDER BY u2.attposition) as referenced_columns
152-
FROM
153-
pg_constraint c
154-
JOIN
155-
pg_namespace n ON n.oid = c.connamespace
156-
JOIN
157-
pg_class t ON t.oid = c.conrelid
158-
JOIN
159-
pg_class ref_table ON ref_table.oid = c.confrelid
160-
JOIN
161-
pg_namespace nr ON nr.oid = ref_table.relnamespace
162-
LEFT JOIN
163-
LATERAL unnest(c.conkey) WITH ORDINALITY AS u(attnum, attposition) ON TRUE
164-
LEFT JOIN
165-
pg_attribute col ON col.attrelid = t.oid AND col.attnum = u.attnum
166-
LEFT JOIN
167-
LATERAL unnest(c.confkey) WITH ORDINALITY AS u2(attnum, attposition) ON TRUE
168-
LEFT JOIN
169-
pg_attribute ref_col ON ref_col.attrelid = c.confrelid AND ref_col.attnum = u2.attnum
170-
WHERE
171-
n.nspname = $1
172-
AND t.relname = $2
173-
AND c.contype = 'f'
174-
GROUP BY
175-
c.conname, nr.nspname, ref_table.relname
176-
ORDER BY
177-
c.conname
178-
"""
179-
foreign_keys = await execute_query(foreign_keys_query, conn_id, [schema_name, table_name])
180-
137+
# CrateDB does not provide foreign key constraints.
138+
foreign_keys = []
139+
181140
for fk in foreign_keys:
182141
fk_info = {
183142
"name": fk['constraint_name'],
@@ -201,8 +160,7 @@ async def list_schemas(conn_id: str):
201160
"""List all non-system schemas in the database."""
202161
query = """
203162
SELECT
204-
schema_name,
205-
obj_description(pg_namespace.oid) as description
163+
schema_name
206164
FROM information_schema.schemata
207165
JOIN pg_namespace ON pg_namespace.nspname = schema_name
208166
WHERE
@@ -217,9 +175,7 @@ async def list_schema_tables(conn_id: str, schema: str):
217175
"""List all tables in a specific schema with their descriptions."""
218176
query = """
219177
SELECT
220-
t.table_name,
221-
obj_description(format('"%s"."%s"', t.table_schema, t.table_name)::regclass::oid) as description,
222-
pg_stat_get_tuples_inserted(format('"%s"."%s"', t.table_schema, t.table_name)::regclass::oid) as total_rows
178+
t.table_name
223179
FROM information_schema.tables t
224180
WHERE
225181
t.table_schema = $1
@@ -236,8 +192,7 @@ async def get_table_columns(conn_id: str, schema: str, table: str):
236192
c.column_name,
237193
c.data_type,
238194
c.is_nullable,
239-
c.column_default,
240-
col_description(format('"%s"."%s"', c.table_schema, c.table_name)::regclass::oid, c.ordinal_position) as description
195+
c.column_default
241196
FROM information_schema.columns c
242197
WHERE
243198
c.table_schema = $1 AND
@@ -252,10 +207,7 @@ async def get_table_indexes(conn_id: str, schema: str, table: str):
252207
query = """
253208
SELECT
254209
i.relname as index_name,
255-
pg_get_indexdef(i.oid) as index_definition,
256-
obj_description(i.oid) as description,
257210
am.amname as index_type,
258-
ARRAY_AGG(a.attname ORDER BY k.i) as column_names,
259211
ix.indisunique as is_unique,
260212
ix.indisprimary as is_primary,
261213
ix.indisexclusion as is_exclusion
@@ -270,9 +222,7 @@ async def get_table_indexes(conn_id: str, schema: str, table: str):
270222
JOIN
271223
pg_am am ON i.relam = am.oid
272224
LEFT JOIN
273-
LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, i) ON TRUE
274-
LEFT JOIN
275-
pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
225+
pg_attribute a ON a.attrelid = t.oid
276226
WHERE
277227
n.nspname = $1
278228
AND t.relname = $2
@@ -299,14 +249,11 @@ async def get_table_constraints(conn_id: str, schema: str, table: str):
299249
WHEN c.contype = 'x' THEN 'EXCLUSION'
300250
ELSE 'OTHER'
301251
END as constraint_type_desc,
302-
obj_description(c.oid) as description,
303-
pg_get_constraintdef(c.oid) as definition,
304252
CASE
305253
WHEN c.contype = 'f' THEN
306254
(SELECT nspname FROM pg_namespace WHERE oid = ref_table.relnamespace) || '.' || ref_table.relname
307255
ELSE NULL
308-
END as referenced_table,
309-
ARRAY_AGG(col.attname ORDER BY u.attposition) as column_names
256+
END as referenced_table
310257
FROM
311258
pg_constraint c
312259
JOIN
@@ -316,9 +263,7 @@ async def get_table_constraints(conn_id: str, schema: str, table: str):
316263
LEFT JOIN
317264
pg_class ref_table ON ref_table.oid = c.confrelid
318265
LEFT JOIN
319-
LATERAL unnest(c.conkey) WITH ORDINALITY AS u(attnum, attposition) ON TRUE
320-
LEFT JOIN
321-
pg_attribute col ON col.attrelid = t.oid AND col.attnum = u.attnum
266+
pg_attribute col ON col.attrelid = t.oid
322267
WHERE
323268
n.nspname = $1
324269
AND t.relname = $2
@@ -335,8 +280,6 @@ async def get_index_details(conn_id: str, schema: str, table: str, index: str):
335280
query = """
336281
SELECT
337282
i.relname as index_name,
338-
pg_get_indexdef(i.oid) as index_definition,
339-
obj_description(i.oid) as description,
340283
am.amname as index_type,
341284
ix.indisunique as is_unique,
342285
ix.indisprimary as is_primary,
@@ -345,9 +288,7 @@ async def get_index_details(conn_id: str, schema: str, table: str, index: str):
345288
ix.indisclustered as is_clustered,
346289
ix.indisvalid as is_valid,
347290
i.relpages as pages,
348-
i.reltuples as rows,
349-
ARRAY_AGG(a.attname ORDER BY k.i) as column_names,
350-
ARRAY_AGG(pg_get_indexdef(i.oid, k.i, false) ORDER BY k.i) as column_expressions
291+
i.reltuples as rows
351292
FROM
352293
pg_index ix
353294
JOIN
@@ -359,9 +300,7 @@ async def get_index_details(conn_id: str, schema: str, table: str, index: str):
359300
JOIN
360301
pg_am am ON i.relam = am.oid
361302
LEFT JOIN
362-
LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, i) ON TRUE
363-
LEFT JOIN
364-
pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
303+
pg_attribute a ON a.attrelid = t.oid
365304
WHERE
366305
n.nspname = $1
367306
AND t.relname = $2
@@ -389,17 +328,14 @@ async def get_constraint_details(conn_id: str, schema: str, table: str, constrai
389328
WHEN c.contype = 'x' THEN 'EXCLUSION'
390329
ELSE 'OTHER'
391330
END as constraint_type_desc,
392-
obj_description(c.oid) as description,
393-
pg_get_constraintdef(c.oid) as definition,
394331
CASE
395332
WHEN c.contype = 'f' THEN
396333
(SELECT nspname FROM pg_namespace WHERE oid = ref_table.relnamespace) || '.' || ref_table.relname
397334
ELSE NULL
398335
END as referenced_table,
399-
ARRAY_AGG(col.attname ORDER BY u.attposition) as column_names,
400336
CASE
401337
WHEN c.contype = 'f' THEN
402-
ARRAY_AGG(ref_col.attname ORDER BY u2.attposition)
338+
ARRAY_AGG(ref_col.attname)
403339
ELSE NULL
404340
END as referenced_columns
405341
FROM
@@ -411,13 +347,9 @@ async def get_constraint_details(conn_id: str, schema: str, table: str, constrai
411347
LEFT JOIN
412348
pg_class ref_table ON ref_table.oid = c.confrelid
413349
LEFT JOIN
414-
LATERAL unnest(c.conkey) WITH ORDINALITY AS u(attnum, attposition) ON TRUE
415-
LEFT JOIN
416-
pg_attribute col ON col.attrelid = t.oid AND col.attnum = u.attnum
417-
LEFT JOIN
418-
LATERAL unnest(c.confkey) WITH ORDINALITY AS u2(attnum, attposition) ON c.contype = 'f'
350+
pg_attribute col ON col.attrelid = t.oid
419351
LEFT JOIN
420-
pg_attribute ref_col ON c.contype = 'f' AND ref_col.attrelid = c.confrelid AND ref_col.attnum = u2.attnum
352+
pg_attribute ref_col ON c.contype = 'f' AND ref_col.attrelid = c.confrelid
421353
WHERE
422354
n.nspname = $1
423355
AND t.relname = $2

server/tools/query.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ async def pg_explain(query: str, conn_id: str, params=None):
7878
Complete JSON-formatted execution plan
7979
"""
8080
# Prepend EXPLAIN to the query
81-
explain_query = f"EXPLAIN (FORMAT JSON) {query}"
81+
explain_query = f"EXPLAIN {query}"
8282

8383
# Execute the explain query
8484
result = await execute_query(explain_query, conn_id, params)

0 commit comments

Comments
 (0)