@dypai-ai/mcp 1.4.6 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.js +210 -467
- package/src/tools/introspect.js +311 -0
- package/src/tools/manage-database.js +536 -0
- package/src/tools/run-migration.js +269 -0
- package/src/tools/sql-guard.js +164 -0
- package/src/tools/sync/codec.js +2 -1
- package/src/tools/sync/pull.js +19 -3
- package/src/tools/sync/transforms.js +10 -2
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* introspect — read-only schema introspection against the project DB.
|
|
3
|
+
*
|
|
4
|
+
* Single tool, three targets:
|
|
5
|
+
*
|
|
6
|
+
* target='schema', name='public' → list tables/views/functions/types in the schema
|
|
7
|
+
* target='table', name='public.users' → full DDL-ish payload (columns, PK, FKs, indexes, triggers, RLS)
|
|
8
|
+
* target='function', name='public.fn(int,text)' OR 'public.fn' → signature(s) + body
|
|
9
|
+
*
|
|
10
|
+
* Why a dedicated tool when execute_sql exists:
|
|
11
|
+
* - Saves the agent from writing pg_catalog SQL by hand every time (a common
|
|
12
|
+
* "I gave up and grepped the schema.sql" scenario).
|
|
13
|
+
* - Returns structured JSON, not the raw column dump execute_sql gives back.
|
|
14
|
+
* - Works against schemas `schema.sql` doesn't dump (auth, storage) — the
|
|
15
|
+
* bug-hunt for `operator does not exist: text = uuid` in stock-infomarket
|
|
16
|
+
* burned ~30min because `auth.users.id` type was invisible.
|
|
17
|
+
* - Dog-fooded: the agent can now answer "what signature does function X
|
|
18
|
+
* have?" without reinventing pg_proc joins every session.
|
|
19
|
+
*
|
|
20
|
+
* Security: pure SELECT against pg_catalog / information_schema. The guard
|
|
21
|
+
* still runs so malformed `name` inputs can't inject DML.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { proxyToolCall } from "./proxy.js"
|
|
25
|
+
import { validateSql, formatValidationError } from "./sql-guard.js"
|
|
26
|
+
|
|
27
|
+
function parseTableName(name) {
|
|
28
|
+
const m = String(name).trim().match(/^([a-zA-Z_][\w$]*)(?:\.([a-zA-Z_][\w$]*))?$/)
|
|
29
|
+
if (!m) return null
|
|
30
|
+
return m[2]
|
|
31
|
+
? { schema: m[1], table: m[2] }
|
|
32
|
+
: { schema: "public", table: m[1] }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// function_name can be bare ("foo"), schema-qualified ("public.foo"), or
|
|
36
|
+
// include a signature ("public.foo(int,text)"). When signature is given we
|
|
37
|
+
// filter pg_get_function_identity_arguments to pick the exact overload.
|
|
38
|
+
function parseFunctionName(name) {
|
|
39
|
+
const s = String(name).trim()
|
|
40
|
+
const sigM = s.match(/^([^(]+)\((.*)\)$/)
|
|
41
|
+
const ident = sigM ? sigM[1].trim() : s
|
|
42
|
+
const sig = sigM ? sigM[2].trim() : null
|
|
43
|
+
const m = ident.match(/^([a-zA-Z_][\w$]*)(?:\.([a-zA-Z_][\w$]*))?$/)
|
|
44
|
+
if (!m) return null
|
|
45
|
+
return m[2]
|
|
46
|
+
? { schema: m[1], func: m[2], signature: sig }
|
|
47
|
+
: { schema: "public", func: m[1], signature: sig }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function runSelect(project_id, sql) {
|
|
51
|
+
const v = validateSql(sql, { allowSelectOnly: true })
|
|
52
|
+
if (!v.ok) {
|
|
53
|
+
const msg = formatValidationError(v)
|
|
54
|
+
throw new Error(`introspect: ${msg}`)
|
|
55
|
+
}
|
|
56
|
+
const args = project_id ? { project_id, sql } : { sql }
|
|
57
|
+
const res = await proxyToolCall("execute_sql", args)
|
|
58
|
+
return Array.isArray(res?.rows) ? res.rows : (Array.isArray(res) ? res : [])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function describeSchema(project_id, schema) {
|
|
62
|
+
const s = schema.replace(/'/g, "''")
|
|
63
|
+
const [tables, views, functions] = await Promise.all([
|
|
64
|
+
runSelect(project_id, `
|
|
65
|
+
SELECT tablename AS name
|
|
66
|
+
FROM pg_catalog.pg_tables
|
|
67
|
+
WHERE schemaname = '${s}'
|
|
68
|
+
ORDER BY tablename
|
|
69
|
+
`),
|
|
70
|
+
runSelect(project_id, `
|
|
71
|
+
SELECT viewname AS name
|
|
72
|
+
FROM pg_catalog.pg_views
|
|
73
|
+
WHERE schemaname = '${s}'
|
|
74
|
+
ORDER BY viewname
|
|
75
|
+
`),
|
|
76
|
+
runSelect(project_id, `
|
|
77
|
+
SELECT p.proname AS name,
|
|
78
|
+
pg_catalog.pg_get_function_identity_arguments(p.oid) AS args,
|
|
79
|
+
pg_catalog.pg_get_function_result(p.oid) AS returns
|
|
80
|
+
FROM pg_catalog.pg_proc p
|
|
81
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
|
|
82
|
+
WHERE n.nspname = '${s}'
|
|
83
|
+
AND p.prokind IN ('f','p')
|
|
84
|
+
ORDER BY p.proname, args
|
|
85
|
+
`),
|
|
86
|
+
])
|
|
87
|
+
return {
|
|
88
|
+
schema,
|
|
89
|
+
tables: tables.map(r => r.name),
|
|
90
|
+
views: views.map(r => r.name),
|
|
91
|
+
functions: functions.map(r => ({
|
|
92
|
+
name: r.name,
|
|
93
|
+
args: r.args,
|
|
94
|
+
returns: r.returns,
|
|
95
|
+
})),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function describeTable(project_id, schema, table) {
|
|
100
|
+
const s = schema.replace(/'/g, "''")
|
|
101
|
+
const t = table.replace(/'/g, "''")
|
|
102
|
+
|
|
103
|
+
const [columns, pk, fks, indexes, triggers, rls, rowcount] = await Promise.all([
|
|
104
|
+
runSelect(project_id, `
|
|
105
|
+
SELECT column_name, data_type, is_nullable,
|
|
106
|
+
column_default, character_maximum_length, udt_name
|
|
107
|
+
FROM information_schema.columns
|
|
108
|
+
WHERE table_schema = '${s}' AND table_name = '${t}'
|
|
109
|
+
ORDER BY ordinal_position
|
|
110
|
+
`),
|
|
111
|
+
runSelect(project_id, `
|
|
112
|
+
SELECT kcu.column_name
|
|
113
|
+
FROM information_schema.table_constraints tc
|
|
114
|
+
JOIN information_schema.key_column_usage kcu
|
|
115
|
+
ON kcu.constraint_name = tc.constraint_name
|
|
116
|
+
AND kcu.table_schema = tc.table_schema
|
|
117
|
+
WHERE tc.table_schema = '${s}' AND tc.table_name = '${t}'
|
|
118
|
+
AND tc.constraint_type = 'PRIMARY KEY'
|
|
119
|
+
ORDER BY kcu.ordinal_position
|
|
120
|
+
`),
|
|
121
|
+
runSelect(project_id, `
|
|
122
|
+
SELECT kcu.column_name,
|
|
123
|
+
ccu.table_schema AS foreign_schema,
|
|
124
|
+
ccu.table_name AS foreign_table,
|
|
125
|
+
ccu.column_name AS foreign_column,
|
|
126
|
+
rc.update_rule, rc.delete_rule
|
|
127
|
+
FROM information_schema.table_constraints tc
|
|
128
|
+
JOIN information_schema.key_column_usage kcu
|
|
129
|
+
ON kcu.constraint_name = tc.constraint_name
|
|
130
|
+
AND kcu.table_schema = tc.table_schema
|
|
131
|
+
JOIN information_schema.referential_constraints rc
|
|
132
|
+
ON rc.constraint_name = tc.constraint_name
|
|
133
|
+
AND rc.constraint_schema = tc.table_schema
|
|
134
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
135
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
136
|
+
AND ccu.table_schema = tc.table_schema
|
|
137
|
+
WHERE tc.table_schema = '${s}' AND tc.table_name = '${t}'
|
|
138
|
+
AND tc.constraint_type = 'FOREIGN KEY'
|
|
139
|
+
ORDER BY kcu.ordinal_position
|
|
140
|
+
`),
|
|
141
|
+
runSelect(project_id, `
|
|
142
|
+
SELECT indexname AS name, indexdef AS definition
|
|
143
|
+
FROM pg_catalog.pg_indexes
|
|
144
|
+
WHERE schemaname = '${s}' AND tablename = '${t}'
|
|
145
|
+
ORDER BY indexname
|
|
146
|
+
`),
|
|
147
|
+
runSelect(project_id, `
|
|
148
|
+
SELECT trigger_name AS name, event_manipulation AS event,
|
|
149
|
+
action_timing AS timing, action_statement AS body
|
|
150
|
+
FROM information_schema.triggers
|
|
151
|
+
WHERE event_object_schema = '${s}' AND event_object_table = '${t}'
|
|
152
|
+
ORDER BY trigger_name
|
|
153
|
+
`),
|
|
154
|
+
runSelect(project_id, `
|
|
155
|
+
SELECT polname AS name, polcmd AS command,
|
|
156
|
+
pg_catalog.pg_get_expr(polqual, polrelid) AS using_expr,
|
|
157
|
+
pg_catalog.pg_get_expr(polwithcheck, polrelid) AS check_expr,
|
|
158
|
+
(SELECT relrowsecurity FROM pg_catalog.pg_class c
|
|
159
|
+
WHERE c.relname='${t}'
|
|
160
|
+
AND c.relnamespace=(SELECT oid FROM pg_catalog.pg_namespace WHERE nspname='${s}')) AS rls_enabled
|
|
161
|
+
FROM pg_catalog.pg_policy p
|
|
162
|
+
JOIN pg_catalog.pg_class c ON c.oid = p.polrelid
|
|
163
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
164
|
+
WHERE n.nspname = '${s}' AND c.relname = '${t}'
|
|
165
|
+
ORDER BY polname
|
|
166
|
+
`),
|
|
167
|
+
runSelect(project_id, `
|
|
168
|
+
SELECT reltuples::bigint AS approximate_rows
|
|
169
|
+
FROM pg_catalog.pg_class c
|
|
170
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
171
|
+
WHERE n.nspname = '${s}' AND c.relname = '${t}'
|
|
172
|
+
LIMIT 1
|
|
173
|
+
`),
|
|
174
|
+
])
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
schema, table,
|
|
178
|
+
exists: columns.length > 0,
|
|
179
|
+
approximate_rows: rowcount[0]?.approximate_rows ?? null,
|
|
180
|
+
columns,
|
|
181
|
+
primary_key: pk.map(r => r.column_name),
|
|
182
|
+
foreign_keys: fks,
|
|
183
|
+
indexes,
|
|
184
|
+
triggers,
|
|
185
|
+
row_level_security: {
|
|
186
|
+
enabled: rls[0]?.rls_enabled === true,
|
|
187
|
+
policies: rls,
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function describeFunction(project_id, schema, funcName, signature) {
|
|
193
|
+
const s = schema.replace(/'/g, "''")
|
|
194
|
+
const f = funcName.replace(/'/g, "''")
|
|
195
|
+
let overloads = await runSelect(project_id, `
|
|
196
|
+
SELECT p.oid,
|
|
197
|
+
p.proname AS name,
|
|
198
|
+
pg_catalog.pg_get_function_identity_arguments(p.oid) AS args,
|
|
199
|
+
pg_catalog.pg_get_function_result(p.oid) AS returns,
|
|
200
|
+
p.prokind,
|
|
201
|
+
l.lanname AS language,
|
|
202
|
+
p.prosecdef AS security_definer,
|
|
203
|
+
p.provolatile AS volatile_flag,
|
|
204
|
+
pg_catalog.pg_get_functiondef(p.oid) AS definition
|
|
205
|
+
FROM pg_catalog.pg_proc p
|
|
206
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
|
|
207
|
+
JOIN pg_catalog.pg_language l ON l.oid = p.prolang
|
|
208
|
+
WHERE n.nspname = '${s}' AND p.proname = '${f}'
|
|
209
|
+
AND p.prokind IN ('f','p')
|
|
210
|
+
ORDER BY args
|
|
211
|
+
`)
|
|
212
|
+
|
|
213
|
+
if (signature != null) {
|
|
214
|
+
// Normalize whitespace for comparison — user might write
|
|
215
|
+
// "int,text" or "integer , text".
|
|
216
|
+
const norm = sig => sig.replace(/\s+/g, "").toLowerCase()
|
|
217
|
+
const wanted = norm(signature)
|
|
218
|
+
overloads = overloads.filter(o => norm(o.args) === wanted)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
schema,
|
|
223
|
+
name: funcName,
|
|
224
|
+
requested_signature: signature ?? null,
|
|
225
|
+
overloads: overloads.map(o => ({
|
|
226
|
+
signature: `${schema}.${funcName}(${o.args})`,
|
|
227
|
+
args: o.args,
|
|
228
|
+
returns: o.returns,
|
|
229
|
+
kind: o.prokind === "p" ? "procedure" : "function",
|
|
230
|
+
language: o.language,
|
|
231
|
+
security_definer: o.security_definer,
|
|
232
|
+
volatile: o.volatile_flag === "i" ? "immutable" : o.volatile_flag === "s" ? "stable" : "volatile",
|
|
233
|
+
definition: o.definition,
|
|
234
|
+
})),
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export const introspectTool = {
|
|
239
|
+
name: "introspect",
|
|
240
|
+
description:
|
|
241
|
+
"Read-only introspection of the project database. One tool, three targets:\n\n" +
|
|
242
|
+
"• target='schema' name='public' — list tables/views/functions in a schema\n" +
|
|
243
|
+
"• target='table' name='public.orders' — columns, PK, FKs, indexes, triggers, RLS, approx rows\n" +
|
|
244
|
+
"• target='function' name='public.foo' — all overloads with signature + body\n" +
|
|
245
|
+
" (or disambiguate an overload: name='public.foo(uuid,text)')\n\n" +
|
|
246
|
+
"Prefer this over hand-rolling pg_catalog queries. Works against ALL schemas including " +
|
|
247
|
+
"`auth`, `storage`, `system` (read-only). Returns structured JSON, never raw SQL dumps.",
|
|
248
|
+
inputSchema: {
|
|
249
|
+
type: "object",
|
|
250
|
+
properties: {
|
|
251
|
+
project_id: {
|
|
252
|
+
type: "string",
|
|
253
|
+
description: "Project UUID. Optional if your token is project-scoped.",
|
|
254
|
+
},
|
|
255
|
+
target: {
|
|
256
|
+
type: "string",
|
|
257
|
+
enum: ["schema", "table", "function"],
|
|
258
|
+
description: "What kind of object to inspect.",
|
|
259
|
+
},
|
|
260
|
+
name: {
|
|
261
|
+
type: "string",
|
|
262
|
+
description:
|
|
263
|
+
"Identifier of the object. Bare name is treated as `public.<name>`. " +
|
|
264
|
+
"For function overloads include the signature: `schema.foo(type1,type2)`.",
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
required: ["target", "name"],
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
async execute({ project_id, target, name } = {}) {
|
|
271
|
+
if (!target || !name) {
|
|
272
|
+
return { success: false, error: "target and name are required." }
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
if (target === "schema") {
|
|
276
|
+
const parsed = parseTableName(name)
|
|
277
|
+
if (!parsed) return { success: false, error: `Invalid schema name: ${name}` }
|
|
278
|
+
// Accept "public" or "public.x" — we use the first identifier as schema
|
|
279
|
+
const data = await describeSchema(project_id, parsed.schema)
|
|
280
|
+
return { success: true, ...data }
|
|
281
|
+
}
|
|
282
|
+
if (target === "table") {
|
|
283
|
+
const parsed = parseTableName(name)
|
|
284
|
+
if (!parsed) return { success: false, error: `Invalid table name: ${name}` }
|
|
285
|
+
const data = await describeTable(project_id, parsed.schema, parsed.table)
|
|
286
|
+
if (!data.exists) {
|
|
287
|
+
return { success: false, error: `Table ${parsed.schema}.${parsed.table} does not exist.`, ...data }
|
|
288
|
+
}
|
|
289
|
+
return { success: true, ...data }
|
|
290
|
+
}
|
|
291
|
+
if (target === "function") {
|
|
292
|
+
const parsed = parseFunctionName(name)
|
|
293
|
+
if (!parsed) return { success: false, error: `Invalid function name: ${name}` }
|
|
294
|
+
const data = await describeFunction(project_id, parsed.schema, parsed.func, parsed.signature)
|
|
295
|
+
if (!data.overloads.length) {
|
|
296
|
+
return {
|
|
297
|
+
success: false,
|
|
298
|
+
error: parsed.signature
|
|
299
|
+
? `No function ${parsed.schema}.${parsed.func}(${parsed.signature}) — signature not found.`
|
|
300
|
+
: `No function ${parsed.schema}.${parsed.func} exists.`,
|
|
301
|
+
hint: "Use introspect(target='schema', name='<schema>') to list functions.",
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return { success: true, ...data }
|
|
305
|
+
}
|
|
306
|
+
return { success: false, error: `Unknown target: ${target}` }
|
|
307
|
+
} catch (e) {
|
|
308
|
+
return { success: false, error: e.message }
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
}
|