@dypai-ai/mcp 1.4.5 → 1.5.0

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.
@@ -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
+ }