@dypai-ai/mcp 1.5.4 → 1.5.6
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
CHANGED
package/src/index.js
CHANGED
|
@@ -115,6 +115,7 @@ const REMOTE_TOOLS = [
|
|
|
115
115
|
// ── Project ───────────────────────────────────────────────────────────────
|
|
116
116
|
{ name: "list_projects", description: "Lists all projects you have access to across your organizations. Returns project id, name, description, organization, subscription plan, and status. Use this as the first step to discover which projects are available, then pass project_id to other tools.", inputSchema: { type: "object", properties: { organization_id: { type: "string", description: "Optional. Filter projects by organization UUID." } }, required: [] } },
|
|
117
117
|
{ name: "get_project", description: "Gets detailed information about a specific project. Returns project name, description, organization, plan, status, engine URL, frontend slug, and timestamps.", inputSchema: { type: "object", properties: { project_id: { type: "string" } }, required: ["project_id"] } },
|
|
118
|
+
{ name: "list_ai_models", description: "List only the DYPAI Managed AI models that are active for a project. Returns the project-gated OpenRouter model catalog, monthly included AI credits, monthly hard cap, RPM limit, max output tokens, active/available counts, and the exact node parameters to use. Call this before creating or editing an AI Agent node with DYPAI Managed models. Agents must not invent or use inactive model ids. Use provider='openrouter' and do NOT set credential_id; DYPAI uses the platform OpenRouter key and bills usage to the organization.", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "Project UUID whose plan and Model Gateway settings determine the active Managed AI catalog." } }, required: ["project_id"] } },
|
|
118
119
|
{ name: "create_project", description: "Create a new DYPAI project (free plan). Creates a full project with database, engine, GitHub repo, and frontend hosting. BLOCKS by default until provisioning finishes (~60s typical, 120s max) — when it returns, the project_id is ready to use with execute_sql, endpoint tools, etc. Pass wait_until_ready:false for batch flows.\n\nName collision: if another project in the same org already uses the name (case-insensitive), returns {error:'name_taken', existing_project_id, suggestions:[...]}. Pick a different name or use the existing project.\n\nIMPORTANT: before calling, check for a matching template with `search_project_templates`. Passing a `template_slug` drops in a ready-made schema + endpoints + UI that cover 70% of common app types. Only create a blank project if nothing matches.", inputSchema: { type: "object", properties: { name: { type: "string", description: "Project name (e.g. 'My Veterinary App')" }, organization_id: { type: "string", description: "Optional. Uses default org if omitted." }, description: { type: "string" }, template_slug: { type: "string", description: "RECOMMENDED. Project template slug to start from (e.g. 'clinic', 'gym', 'waitlist', 'blank'). Always call search_project_templates first to find the best match." }, wait_until_ready: { type: "boolean", description: "If true (default), blocks until provisioning completes and the project is ready for all operations. If false, returns immediately with status='provisioning' — caller must poll get_project before using.", default: true } }, required: ["name"] } },
|
|
119
120
|
{ name: "get_app_credentials", description: "Lists available credentials in the current application. Returns API keys, anon key, service role key, and engine URL needed for SDK configuration.", inputSchema: { type: "object", properties: { project_id: { type: "string" } }, required: [] } },
|
|
120
121
|
|
|
@@ -166,7 +167,7 @@ const REMOTE_TOOLS = [
|
|
|
166
167
|
// and fail to learn the contract. Mirrors the validation the remote does.
|
|
167
168
|
{
|
|
168
169
|
name: "manage_users",
|
|
169
|
-
description: `Manage authenticated users
|
|
170
|
+
description: `Manage authenticated users.
|
|
170
171
|
|
|
171
172
|
Operations:
|
|
172
173
|
- list: Paginated user listing. Optional: search, limit (1-100, default 20), offset, sort_by, sort_order.
|
|
@@ -177,14 +178,14 @@ Operations:
|
|
|
177
178
|
- delete: Permanently remove a user (user_id).
|
|
178
179
|
- ban / unban: Block / unblock login (user_id; ban supports ban_reason and ban_expires_in seconds).
|
|
179
180
|
|
|
180
|
-
NOTE: user IDs are TEXT (
|
|
181
|
+
NOTE: user IDs are TEXT alphanumeric strings (~32 chars, like "G1LIBXsbMLxUrs99ebCaL9X4auxW26AC"), NOT UUIDs. They live in auth."user".id.`,
|
|
181
182
|
inputSchema: {
|
|
182
183
|
type: "object",
|
|
183
184
|
properties: {
|
|
184
185
|
project_id: { type: "string", description: "Project UUID. Required for user tokens; auto-detected for project tokens." },
|
|
185
186
|
operation: { type: "string", enum: ["list", "get", "create", "set_password", "update_role", "delete", "ban", "unban"] },
|
|
186
187
|
// Fields used by various operations
|
|
187
|
-
user_id: { type: "string", description: "User ID (TEXT,
|
|
188
|
+
user_id: { type: "string", description: "User ID (TEXT alphanumeric, NOT a UUID; from auth.\"user\".id). Required for: get, set_password, update_role, delete, ban, unban." },
|
|
188
189
|
email: { type: "string", description: "User email. Required for: create." },
|
|
189
190
|
password: { type: "string", description: "User password (min 6 chars). Required for: create, set_password." },
|
|
190
191
|
name: { type: "string", description: "Display name. Optional for: create." },
|
|
@@ -477,7 +478,7 @@ const SERVER_INSTRUCTIONS = `You are building full-stack applications on the DYP
|
|
|
477
478
|
- ❌ *"¿Prefieres Tailwind o CSS Modules?"*
|
|
478
479
|
- ❌ Asking "which framework" unless the user explicitly says *"I want to compare platforms"* or *"what are my options"*.
|
|
479
480
|
|
|
480
|
-
These are ALL dead signals: Next.js is already what DYPAI scaffolds. The DB is already PostgreSQL (via DYPAI engine). Auth is
|
|
481
|
+
These are ALL dead signals: Next.js is already what DYPAI scaffolds. The DB is already PostgreSQL (via DYPAI engine). Auth is built in. Tailwind is already in the templates. Prisma / ORMs are not used — you write SQL directly in workflow endpoints.
|
|
481
482
|
|
|
482
483
|
## What to do when the user says "I want to build X"
|
|
483
484
|
|
|
@@ -861,7 +862,7 @@ Mental translations: "edge function" → workflow with one code node; "cron" →
|
|
|
861
862
|
|
|
862
863
|
## Common app recipes (one-liners)
|
|
863
864
|
|
|
864
|
-
- **Auth-gated CRUD**: table with \`user_id UUID
|
|
865
|
+
- **Auth-gated CRUD**: table with \`user_id TEXT\` (auth IDs are TEXT, not UUID), jwt endpoints, SQL always filters by \`\${current_user_id}\`, frontend \`<ProtectedRoute>\`.
|
|
865
866
|
- **Admin panel**: endpoints with \`allowed_roles: [admin]\`. Same SQL, no user filter (admin sees all). Promote via \`manage_users(update_role)\`.
|
|
866
867
|
- **AI chat**: single endpoint with \`agent\` node + \`memory_key: "chat:\${current_user_id}"\`. Frontend \`dypai.api.stream()\`.
|
|
867
868
|
- **Payments (Stripe)**: \`stripe\` node for ops. Webhook endpoint with \`trigger.webhook\` + \`stripe_webhook: true\` (auto-verifies signature).
|
|
@@ -889,7 +890,7 @@ SDK is pre-configured at \`src/lib/dypai.ts\` (or \`src/dypai.ts\`). Import \`dy
|
|
|
889
890
|
|
|
890
891
|
- \`bulk_upsert\` — CSV/JSON → table. Seed data.
|
|
891
892
|
- \`manage_domain\` — custom domains. \`add\` returns CNAME to configure. \`verify\` rechecks DNS/SSL. → \`search_docs("manage domain")\`.
|
|
892
|
-
- \`manage_users\` / \`manage_roles\` — app-level RBAC
|
|
893
|
+
- \`manage_users\` / \`manage_roles\` — app-level RBAC. Create/ban/assign roles.
|
|
893
894
|
- \`manage_schedules\` / \`manage_webhooks\` — pause/resume/history. To change the DEFINITION, edit the YAML and push.
|
|
894
895
|
- \`manage_storage\` — buckets + objects. \`upload_file\` reads local path, signs URL, PUTs, registers. Max 100MB/file. → \`search_docs("file storage")\`.
|
|
895
896
|
- \`manage_drafts\` — universal draft/publish. \`list\` before \`publish\` so the user sees what's shipping. \`discard\` to throw away. Always pair with \`dypai_test_endpoint(mode:'draft')\` before publish.
|
package/src/tools/proxy.js
CHANGED
|
@@ -16,6 +16,70 @@ const MCP_ENDPOINT = `${MCP_BASE}/mcp`
|
|
|
16
16
|
|
|
17
17
|
let sessionId = null
|
|
18
18
|
|
|
19
|
+
function extractLastSseData(text) {
|
|
20
|
+
const events = text.split(/\r?\n\r?\n/).filter(Boolean)
|
|
21
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
22
|
+
const dataLines = events[i]
|
|
23
|
+
.split(/\r?\n/)
|
|
24
|
+
.filter((line) => line.startsWith("data:"))
|
|
25
|
+
.map((line) => line.replace(/^data:\s?/, ""))
|
|
26
|
+
.filter((line) => line && line !== "[DONE]")
|
|
27
|
+
if (dataLines.length) return dataLines.join("\n")
|
|
28
|
+
}
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function decodeTransportValue(value) {
|
|
33
|
+
let current = value
|
|
34
|
+
for (let i = 0; i < 5; i++) {
|
|
35
|
+
if (typeof current !== "string") return current
|
|
36
|
+
const trimmed = current.trim()
|
|
37
|
+
const sseData = extractLastSseData(trimmed)
|
|
38
|
+
const candidate = sseData || trimmed
|
|
39
|
+
try {
|
|
40
|
+
current = JSON.parse(candidate)
|
|
41
|
+
} catch {
|
|
42
|
+
return current
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return current
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeJsonRpcResponse(response) {
|
|
49
|
+
let current = decodeTransportValue(response)
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < 5; i++) {
|
|
52
|
+
if (!current || typeof current !== "object") return current
|
|
53
|
+
|
|
54
|
+
// Some HTTP/SSE transports get wrapped as { result: "<jsonrpc...>" } after
|
|
55
|
+
// a fallback parse path. Unwrap that so callers always see the real MCP
|
|
56
|
+
// JSON-RPC response instead of a stringified envelope.
|
|
57
|
+
if (
|
|
58
|
+
Object.keys(current).length === 1
|
|
59
|
+
&& typeof current.result === "string"
|
|
60
|
+
) {
|
|
61
|
+
const decoded = decodeTransportValue(current.result)
|
|
62
|
+
if (decoded !== current.result) {
|
|
63
|
+
current = decoded
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
current.result
|
|
70
|
+
&& typeof current.result === "object"
|
|
71
|
+
&& current.result.jsonrpc
|
|
72
|
+
) {
|
|
73
|
+
current = current.result
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return current
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return current
|
|
81
|
+
}
|
|
82
|
+
|
|
19
83
|
function mcpRequest(body) {
|
|
20
84
|
const token = process.env.DYPAI_TOKEN || ""
|
|
21
85
|
|
|
@@ -50,18 +114,9 @@ function mcpRequest(body) {
|
|
|
50
114
|
res.on("end", () => {
|
|
51
115
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
52
116
|
// Handle SSE responses (text/event-stream)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (lastData) {
|
|
57
|
-
try {
|
|
58
|
-
resolve(JSON.parse(lastData.replace("data: ", "")))
|
|
59
|
-
} catch {
|
|
60
|
-
resolve({ result: buf })
|
|
61
|
-
}
|
|
62
|
-
} else {
|
|
63
|
-
resolve({ result: buf })
|
|
64
|
-
}
|
|
117
|
+
const sseData = extractLastSseData(buf)
|
|
118
|
+
if (sseData) {
|
|
119
|
+
try { resolve(JSON.parse(sseData)) } catch { resolve({ result: sseData }) }
|
|
65
120
|
} else {
|
|
66
121
|
try { resolve(JSON.parse(buf)) } catch { resolve({ result: buf }) }
|
|
67
122
|
}
|
|
@@ -116,7 +171,7 @@ async function ensureInitialized() {
|
|
|
116
171
|
export async function proxyToolCall(toolName, args) {
|
|
117
172
|
await ensureInitialized()
|
|
118
173
|
|
|
119
|
-
const response = await mcpRequest({
|
|
174
|
+
const response = normalizeJsonRpcResponse(await mcpRequest({
|
|
120
175
|
jsonrpc: "2.0",
|
|
121
176
|
id: `proxy-${Date.now()}`,
|
|
122
177
|
method: "tools/call",
|
|
@@ -124,7 +179,7 @@ export async function proxyToolCall(toolName, args) {
|
|
|
124
179
|
name: toolName,
|
|
125
180
|
arguments: args || {},
|
|
126
181
|
},
|
|
127
|
-
})
|
|
182
|
+
}))
|
|
128
183
|
|
|
129
184
|
// Extract result from JSON-RPC response
|
|
130
185
|
if (response.result) {
|
|
@@ -137,7 +192,7 @@ export async function proxyToolCall(toolName, args) {
|
|
|
137
192
|
|
|
138
193
|
if (text != null) {
|
|
139
194
|
let parsed
|
|
140
|
-
|
|
195
|
+
parsed = decodeTransportValue(text)
|
|
141
196
|
// Some remote errors come as a plain string without isError flag
|
|
142
197
|
if (typeof parsed === "string" && /^(Error:|Input validation error:|You don't have)/i.test(parsed)) {
|
|
143
198
|
throw new Error(parsed)
|
package/src/tools/sql-guard.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
// Schemas the agent must not modify. SELECT against them stays allowed.
|
|
20
20
|
const PROTECTED_SCHEMAS = new Set([
|
|
21
|
-
"auth", //
|
|
21
|
+
"auth", // auth user/session tables — engine-owned
|
|
22
22
|
"storage", // file upload metadata — engine-owned
|
|
23
23
|
"system", // DYPAI internals (endpoints, credentials, etc.)
|
|
24
24
|
"information_schema", // PG metadata catalog
|
|
@@ -127,7 +127,7 @@ export const dypaiDescribeTool = {
|
|
|
127
127
|
placeholders_available: [
|
|
128
128
|
"${input.<field>} — request body / query params",
|
|
129
129
|
"${nodes.<node_id>.<field>} — output of a previous node",
|
|
130
|
-
"${current_user_id} —
|
|
130
|
+
"${current_user_id} — id of the authenticated user (TEXT, NOT a UUID; jwt auth only)",
|
|
131
131
|
"${current_user_role} — role name of the authenticated user",
|
|
132
132
|
],
|
|
133
133
|
|
package/src/tools/sync/pull.js
CHANGED
|
@@ -206,7 +206,7 @@ output:
|
|
|
206
206
|
#
|
|
207
207
|
# \${input.<field>} — the request body / query params
|
|
208
208
|
# \${nodes.<id>.<field>} — output of a previous node
|
|
209
|
-
# \${current_user_id} —
|
|
209
|
+
# \${current_user_id} — ID of the JWT-authenticated user (TEXT, NOT a UUID — match against TEXT columns)
|
|
210
210
|
# \${current_user_role} — role name from the JWT
|
|
211
211
|
#
|
|
212
212
|
# Expressions inside placeholders work: arithmetic, comparisons, JS-ish.
|
|
@@ -218,8 +218,9 @@ output:
|
|
|
218
218
|
# plain object/array → binds as ::jsonb
|
|
219
219
|
# Date instance → binds as ::timestamptz
|
|
220
220
|
# text / int / bool → binds without an explicit cast (Postgres infers)
|
|
221
|
-
# So you write SQL naturally — DON'T add
|
|
222
|
-
# Just write: WHERE
|
|
221
|
+
# So you write SQL naturally — DON'T add type casts manually like
|
|
222
|
+
# '\${input.product_id}'::uuid. Just write: WHERE id = \${input.product_id}
|
|
223
|
+
# (and remember: \${current_user_id} is TEXT, so user_id columns must be TEXT too).
|
|
223
224
|
workflow:
|
|
224
225
|
nodes:
|
|
225
226
|
|
|
@@ -243,7 +243,7 @@ export const dypaiTestEndpointTool = {
|
|
|
243
243
|
},
|
|
244
244
|
as_user: {
|
|
245
245
|
type: "string",
|
|
246
|
-
description: "User
|
|
246
|
+
description: "User ID to impersonate. Required for jwt endpoints that read ${current_user_id}. NOTE: user IDs are stored in auth.\"user\".id as a TEXT alphanumeric string (e.g. '9KxggvkPhgpYXITE6koY0vYWJqQzSwaw'), NOT a UUID. Discover IDs with manage_users(operation:'list') or execute_sql(\"SELECT id, email FROM auth.\\\"user\\\" LIMIT 5\"). Common mistake: passing a UUID copied from system.users or a business table.",
|
|
247
247
|
},
|
|
248
248
|
trace_mode: {
|
|
249
249
|
type: "string",
|
|
@@ -1175,7 +1175,7 @@ export async function runValidation(rootDir, projectId) {
|
|
|
1175
1175
|
` user_id TEXT NOT NULL,\\n` +
|
|
1176
1176
|
` created_at TIMESTAMPTZ DEFAULT NOW()\\n` +
|
|
1177
1177
|
` );" })\n` +
|
|
1178
|
-
`IMPORTANT: user_id must be TEXT (not UUID) —
|
|
1178
|
+
`IMPORTANT: user_id must be TEXT (not UUID) — auth.\"user\".id is a TEXT alphanumeric string (~32 chars). ` +
|
|
1179
1179
|
`(Add other columns matching what your endpoint INSERTs.) ` +
|
|
1180
1180
|
`schema.sql will auto-refresh after the DDL. Existing tables: ${knownTablesList}`,
|
|
1181
1181
|
})
|