@dypai-ai/mcp 1.2.3 → 1.3.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.
- package/package.json +1 -1
- package/src/index.js +535 -15
- package/src/tools/bulk-upsert.js +69 -12
- package/src/tools/frontend.js +37 -11
- package/src/tools/proxy.js +33 -20
- package/src/tools/scaffold.js +33 -3
- package/src/tools/sync/codec.js +160 -16
- package/src/tools/sync/diff.js +8 -0
- package/src/tools/sync/planner.js +16 -1
- package/src/tools/sync/pull.js +363 -3
- package/src/tools/sync/push.js +8 -0
- package/src/tools/sync/test-endpoint.js +76 -3
- package/src/tools/sync/transforms.js +5 -0
- package/src/tools/sync/validate.js +342 -29
- package/src/tools/sync.js +133 -0
- package/src/tools/trace-summarize.js +8 -0
package/src/index.js
CHANGED
|
@@ -25,7 +25,14 @@ import { checkForUpdates } from "./auto-update.js"
|
|
|
25
25
|
// get_build_status, list_deployments, get_deployment_logs) into a single operation-dispatched
|
|
26
26
|
// tool. The heavy deploy logic still lives in ./tools/deploy.js (exported as deployFromSource).
|
|
27
27
|
import { manageFrontendTool } from "./tools/frontend.js"
|
|
28
|
-
|
|
28
|
+
// scaffoldTool (download_template) was removed from the catalog in favor of
|
|
29
|
+
// `manage_frontend(operation: "sync")`, which pulls the project's actual
|
|
30
|
+
// source from GitHub instead of dropping a generic Vite starter on disk.
|
|
31
|
+
// The "create from scratch" use case is owned by the `@dypai-ai/install` CLI
|
|
32
|
+
// (interactive, OAuth, IDE detection, MCP config), which is a better fit for
|
|
33
|
+
// that flow than an MCP tool. The ./tools/scaffold.js file is preserved on
|
|
34
|
+
// disk in case a future v2 wants to resurrect a leaner version.
|
|
35
|
+
// import { scaffoldTool } from "./tools/scaffold.js"
|
|
29
36
|
import { manageDomainTool } from "./tools/domains.js"
|
|
30
37
|
import { bulkUpsertTool } from "./tools/bulk-upsert.js"
|
|
31
38
|
// dypaiTestTool (YAML test-suite runner) is intentionally not imported — deferred to v2.
|
|
@@ -61,8 +68,9 @@ await checkForUpdates().catch(() => {})
|
|
|
61
68
|
|
|
62
69
|
const LOCAL_TOOLS = [
|
|
63
70
|
// ── Frontend & Deploy ─────────────────────────────────────────────────────
|
|
71
|
+
// manage_frontend covers: deploy, sync (pull source from repo), status,
|
|
72
|
+
// build_status, list_deployments, logs.
|
|
64
73
|
manageFrontendTool,
|
|
65
|
-
scaffoldTool,
|
|
66
74
|
// ── Domains ───────────────────────────────────────────────────────────────
|
|
67
75
|
manageDomainTool,
|
|
68
76
|
// ── Data ──────────────────────────────────────────────────────────────────
|
|
@@ -94,7 +102,7 @@ const REMOTE_TOOLS = [
|
|
|
94
102
|
// ── Project ───────────────────────────────────────────────────────────────
|
|
95
103
|
{ 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: [] } },
|
|
96
104
|
{ 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"] } },
|
|
97
|
-
{ name: "create_project", description: "Create a new DYPAI project (free plan). Creates a full project with database,
|
|
105
|
+
{ name: "create_project", description: "Create a new DYPAI project (free plan). Creates a full project with database, engine, and hosting. Provisioning takes ~1 minute.\n\nIMPORTANT: before calling this, 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 (clinic, gym, waitlist, SaaS, e-commerce, landing, etc.). Only create a blank project if nothing matches — starting blank costs the user hours of boilerplate.", 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 for what the user asked. Only omit this / use 'blank' when nothing fits." } }, required: ["name"] } },
|
|
98
106
|
{ 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: [] } },
|
|
99
107
|
|
|
100
108
|
// ── Database ──────────────────────────────────────────────────────────────
|
|
@@ -127,17 +135,249 @@ const REMOTE_TOOLS = [
|
|
|
127
135
|
// (dypai_pull handles this), review with git, then dypai_push. That keeps
|
|
128
136
|
// the change reviewable in git instead of being an invisible DB rewrite.
|
|
129
137
|
// { name: "rollback_endpoint", description: "EMERGENCY rollback ...", inputSchema: { ... } },
|
|
130
|
-
|
|
138
|
+
// search_nodes is intentionally hidden — `dypai/node-catalog.json` is cached
|
|
139
|
+
// locally by `dypai_pull` and contains EVERY node with its full schema. The
|
|
140
|
+
// agent should `Read` that file directly: faster, offline, no token used,
|
|
141
|
+
// and the catalog easily fits in context. Exposing search_nodes alongside
|
|
142
|
+
// it confused agents into doing semantic search before bothering to read
|
|
143
|
+
// the file. Implementation still lives on the remote and can be re-enabled
|
|
144
|
+
// if a pure-vector fallback is ever needed.
|
|
145
|
+
// { name: "search_nodes", description: "Semantic (vector) search over the node catalog ...", inputSchema: { ... } },
|
|
131
146
|
|
|
132
147
|
// ── Auth & Users ──────────────────────────────────────────────────────────
|
|
133
148
|
// Note: `get_auth_users` is intentionally NOT exposed — manage_users covers
|
|
134
149
|
// create/delete/ban/update_role/set_password; for listing/reading, use
|
|
135
150
|
// execute_sql against auth.users (read-only) or manage_users with list op.
|
|
136
|
-
|
|
137
|
-
|
|
151
|
+
// manage_users — discriminated by `operation`. The `allOf` block tells the
|
|
152
|
+
// agent which params each operation requires, so it doesn't have to call
|
|
153
|
+
// and fail to learn the contract. Mirrors the validation the remote does.
|
|
154
|
+
{
|
|
155
|
+
name: "manage_users",
|
|
156
|
+
description: `Manage authenticated users (better-auth backed).
|
|
157
|
+
|
|
158
|
+
Operations:
|
|
159
|
+
- list: Paginated user listing. Optional: search, limit (1-100, default 20), offset, sort_by, sort_order.
|
|
160
|
+
- get: Fetch a single user by user_id.
|
|
161
|
+
- create: Create a user with email + password. Optional: name, role, email_verified, send_invite_email.
|
|
162
|
+
- set_password: Admin-overrides a user's password (user_id + new password).
|
|
163
|
+
- update_role: Change a user's app role (user_id + role; role must exist in system.roles).
|
|
164
|
+
- delete: Permanently remove a user (user_id).
|
|
165
|
+
- ban / unban: Block / unblock login (user_id; ban supports ban_reason and ban_expires_in seconds).
|
|
166
|
+
|
|
167
|
+
NOTE: user IDs are TEXT (better-auth nanoids like "G1LIBXsbMLxUrs99ebCaL9X4auxW26AC"), NOT UUIDs.`,
|
|
168
|
+
inputSchema: {
|
|
169
|
+
type: "object",
|
|
170
|
+
properties: {
|
|
171
|
+
project_id: { type: "string", description: "Project UUID. Required for user tokens; auto-detected for project tokens." },
|
|
172
|
+
operation: { type: "string", enum: ["list", "get", "create", "set_password", "update_role", "delete", "ban", "unban"] },
|
|
173
|
+
// Fields used by various operations
|
|
174
|
+
user_id: { type: "string", description: "User ID (TEXT, better-auth nanoid). Required for: get, set_password, update_role, delete, ban, unban." },
|
|
175
|
+
email: { type: "string", description: "User email. Required for: create." },
|
|
176
|
+
password: { type: "string", description: "User password (min 6 chars). Required for: create, set_password." },
|
|
177
|
+
name: { type: "string", description: "Display name. Optional for: create." },
|
|
178
|
+
role: { type: "string", description: "App role name (must exist in system.roles). Optional for create; required for update_role." },
|
|
179
|
+
email_verified: { type: "boolean", description: "Mark email as verified on create (default false). Optional for: create." },
|
|
180
|
+
send_invite_email: { type: "boolean", description: "If creating without password, send a reset-password invite email (default true). Optional for: create." },
|
|
181
|
+
ban_reason: { type: "string", description: "Reason text. Optional for: ban." },
|
|
182
|
+
ban_expires_in: { type: "integer", description: "Ban duration in seconds. Omit for permanent ban. Optional for: ban." },
|
|
183
|
+
// List pagination/sort fields
|
|
184
|
+
search: { type: "string", description: "Substring filter on email or name. Optional for: list." },
|
|
185
|
+
limit: { type: "integer", description: "Max results 1-100 (default 20). Optional for: list." },
|
|
186
|
+
offset: { type: "integer", description: "Pagination offset (default 0). Optional for: list." },
|
|
187
|
+
sort_by: { type: "string", description: "Sort column (default 'created_at'). Optional for: list." },
|
|
188
|
+
sort_order: { type: "string", enum: ["asc", "desc"], description: "Sort direction (default 'desc'). Optional for: list." },
|
|
189
|
+
},
|
|
190
|
+
required: ["operation"],
|
|
191
|
+
allOf: [
|
|
192
|
+
{ if: { properties: { operation: { const: "get" } }, required: ["operation"] }, then: { required: ["user_id"] } },
|
|
193
|
+
{ if: { properties: { operation: { const: "create" } }, required: ["operation"] }, then: { required: ["email", "password"] } },
|
|
194
|
+
{ if: { properties: { operation: { const: "set_password" } }, required: ["operation"] }, then: { required: ["user_id", "password"] } },
|
|
195
|
+
{ if: { properties: { operation: { const: "update_role" } }, required: ["operation"] }, then: { required: ["user_id", "role"] } },
|
|
196
|
+
{ if: { properties: { operation: { const: "delete" } }, required: ["operation"] }, then: { required: ["user_id"] } },
|
|
197
|
+
{ if: { properties: { operation: { const: "ban" } }, required: ["operation"] }, then: { required: ["user_id"] } },
|
|
198
|
+
{ if: { properties: { operation: { const: "unban" } }, required: ["operation"] }, then: { required: ["user_id"] } },
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
// manage_roles — discriminated by `operation`. Same pattern as manage_users.
|
|
204
|
+
{
|
|
205
|
+
name: "manage_roles",
|
|
206
|
+
description: `Manage app roles used for endpoint access control (allowed_roles in YAML) and user assignment.
|
|
207
|
+
|
|
208
|
+
Operations:
|
|
209
|
+
- list: Returns all roles with permissions.
|
|
210
|
+
- create: Create a role (name required). Built-in: 'authenticated', 'admin'.
|
|
211
|
+
- update: Update name / description / permissions of a role (role_id required).
|
|
212
|
+
- delete: Delete a role (role_id required). Endpoints referencing it lose that role.`,
|
|
213
|
+
inputSchema: {
|
|
214
|
+
type: "object",
|
|
215
|
+
properties: {
|
|
216
|
+
project_id: { type: "string", description: "Project UUID. Required for user tokens; auto-detected for project tokens." },
|
|
217
|
+
operation: { type: "string", enum: ["list", "create", "update", "delete"] },
|
|
218
|
+
role_id: { type: "string", description: "Role UUID. Required for: update, delete." },
|
|
219
|
+
name: { type: "string", description: "Role name. Required for: create. Optional for: update." },
|
|
220
|
+
description: { type: "string", description: "Free-text role description. Optional for: create, update." },
|
|
221
|
+
permissions: { type: "object", description: "JSON of role flags. Keys: manage_users, manage_roles, manage_system. Optional for: create, update." },
|
|
222
|
+
},
|
|
223
|
+
required: ["operation"],
|
|
224
|
+
allOf: [
|
|
225
|
+
{ if: { properties: { operation: { const: "create" } }, required: ["operation"] }, then: { required: ["name"] } },
|
|
226
|
+
{ if: { properties: { operation: { const: "update" } }, required: ["operation"] }, then: { required: ["role_id"] } },
|
|
227
|
+
{ if: { properties: { operation: { const: "delete" } }, required: ["operation"] }, then: { required: ["role_id"] } },
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
},
|
|
138
231
|
|
|
139
232
|
// ── Storage ───────────────────────────────────────────────────────────────
|
|
140
|
-
|
|
233
|
+
// manage_storage covers BOTH bucket-level and object-level operations.
|
|
234
|
+
// The remote also accepts the legacy name `list_buckets` (alias) so older
|
|
235
|
+
// local MCP versions still work during rollout. We expose only the new name.
|
|
236
|
+
{
|
|
237
|
+
name: "manage_storage",
|
|
238
|
+
description: `Manage storage: buckets AND the objects inside them.
|
|
239
|
+
|
|
240
|
+
Bucket operations:
|
|
241
|
+
- list: Returns all buckets (default operation; no args needed).
|
|
242
|
+
- create: Create a bucket. Args: name (lowercase, hyphens, 3-63 chars), public (default false).
|
|
243
|
+
- delete: Delete a bucket and ALL its objects. Args: name. WARNING: irreversible.
|
|
244
|
+
|
|
245
|
+
Object operations (within a specific bucket):
|
|
246
|
+
- list_objects: List files in a bucket. Args: bucket; optional prefix, limit, offset.
|
|
247
|
+
- delete_object: Delete one file. Args: bucket, name.
|
|
248
|
+
- get_signed_download_url: Generate a temporary URL to view/download a file.
|
|
249
|
+
Args: bucket, name; optional expires_minutes (1-1440, default 15),
|
|
250
|
+
download (true = force attachment).
|
|
251
|
+
|
|
252
|
+
Notes:
|
|
253
|
+
- UPLOADS are NOT exposed here. Multipart doesn't fit cleanly over MCP stdio.
|
|
254
|
+
Upload via: SDK frontend (\`dypai.api.upload\`) or the \`dypai_storage\` node in a workflow.
|
|
255
|
+
- Bucket names: lowercase, 3-63 chars, [a-z0-9-]. Use hyphens, not underscores.
|
|
256
|
+
- Protected system buckets (e.g. avatars, public, dypai-storage) cannot be deleted.`,
|
|
257
|
+
inputSchema: {
|
|
258
|
+
type: "object",
|
|
259
|
+
properties: {
|
|
260
|
+
project_id: { type: "string", description: "Project UUID. Required for user tokens; auto-detected for project tokens." },
|
|
261
|
+
operation: {
|
|
262
|
+
type: "string",
|
|
263
|
+
enum: ["list", "create", "delete", "list_objects", "delete_object", "get_signed_download_url"],
|
|
264
|
+
default: "list",
|
|
265
|
+
},
|
|
266
|
+
// Bucket-level
|
|
267
|
+
name: { type: "string", description: "For create/delete (bucket name) OR for delete_object/get_signed_download_url (object's logical name inside the bucket, e.g. 'avatars/u_123.png')." },
|
|
268
|
+
public: { type: "boolean", description: "If true, bucket files are publicly accessible. Used by: create. Default: false." },
|
|
269
|
+
// Object-level
|
|
270
|
+
bucket: { type: "string", description: "Bucket name. Required by: list_objects, delete_object, get_signed_download_url." },
|
|
271
|
+
prefix: { type: "string", description: "Filter objects whose name starts with this prefix (e.g. 'avatars/'). Optional for: list_objects." },
|
|
272
|
+
expires_minutes: { type: "integer", minimum: 1, maximum: 1440, description: "Signed URL TTL in minutes. Optional for: get_signed_download_url. Default: 15." },
|
|
273
|
+
download: { type: "boolean", description: "If true, signed URL forces download (Content-Disposition: attachment). Optional for: get_signed_download_url. Default: false." },
|
|
274
|
+
// Pagination (used by list and list_objects)
|
|
275
|
+
limit: { type: "integer", description: "Max results. list: default 50, max 200. list_objects: default 20, max 100." },
|
|
276
|
+
offset: { type: "integer", description: "Pagination offset. Default 0." },
|
|
277
|
+
},
|
|
278
|
+
required: [],
|
|
279
|
+
allOf: [
|
|
280
|
+
{ if: { properties: { operation: { const: "create" } }, required: ["operation"] }, then: { required: ["name"] } },
|
|
281
|
+
{ if: { properties: { operation: { const: "delete" } }, required: ["operation"] }, then: { required: ["name"] } },
|
|
282
|
+
{ if: { properties: { operation: { const: "list_objects" } }, required: ["operation"] }, then: { required: ["bucket"] } },
|
|
283
|
+
{ if: { properties: { operation: { const: "delete_object" } }, required: ["operation"] }, then: { required: ["bucket", "name"] } },
|
|
284
|
+
{ if: { properties: { operation: { const: "get_signed_download_url" } }, required: ["operation"] }, then: { required: ["bucket", "name"] } },
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
// ── Triggers (Schedules + Webhooks) ──────────────────────────────────────
|
|
290
|
+
// These tools OBSERVE + CONTROL trigger runtime state. The DEFINITION lives
|
|
291
|
+
// in the endpoint YAML (`trigger.schedule` / `trigger.webhook`) — to change
|
|
292
|
+
// a cron expression or webhook path, edit the YAML and `dypai_push`.
|
|
293
|
+
{
|
|
294
|
+
name: "manage_schedules",
|
|
295
|
+
description: `Observe + control cron schedules at runtime, by endpoint name.
|
|
296
|
+
|
|
297
|
+
Schedules are DECLARED in dypai/endpoints/<name>.yaml under \`trigger.schedule\`.
|
|
298
|
+
This tool exists for runtime observability and toggling is_active WITHOUT
|
|
299
|
+
round-tripping the YAML.
|
|
300
|
+
|
|
301
|
+
All per-schedule operations target an endpoint by its \`endpoint_name\` — the
|
|
302
|
+
same name you wrote in the YAML. Internal UUIDs are resolved automatically.
|
|
303
|
+
|
|
304
|
+
Operations:
|
|
305
|
+
- list: All schedules in the project. Optional filter: is_active.
|
|
306
|
+
- get: Full detail of one schedule (cron, timezone, last_run, status).
|
|
307
|
+
- pause: Set is_active=false. Engine immediately stops firing the cron.
|
|
308
|
+
- resume: Set is_active=true. Engine re-arms the BullMQ repeatable job.
|
|
309
|
+
- history: List recent executions from system.schedule_runs.
|
|
310
|
+
|
|
311
|
+
To CHANGE the cron expression, timezone, or payload: edit the endpoint YAML
|
|
312
|
+
and \`dypai_push\`. This tool does NOT modify the schedule definition.`,
|
|
313
|
+
inputSchema: {
|
|
314
|
+
type: "object",
|
|
315
|
+
properties: {
|
|
316
|
+
project_id: { type: "string", description: "Project UUID. Required for user tokens; auto-detected for project tokens." },
|
|
317
|
+
operation: { type: "string", enum: ["list", "get", "pause", "resume", "history"] },
|
|
318
|
+
endpoint_name: { type: "string", description: "Endpoint name as declared in dypai/endpoints/<name>.yaml. Required for: get, pause, resume, history." },
|
|
319
|
+
is_active: { type: "boolean", description: "Filter by active state. Optional for: list." },
|
|
320
|
+
limit: { type: "integer", description: "Max results. list: 1-200 (default 50). history: 1-100 (default 20)." },
|
|
321
|
+
offset: { type: "integer", description: "Pagination offset. Optional for: list, history. Default 0." },
|
|
322
|
+
},
|
|
323
|
+
required: ["operation"],
|
|
324
|
+
allOf: [
|
|
325
|
+
{ if: { properties: { operation: { const: "get" } }, required: ["operation"] }, then: { required: ["endpoint_name"] } },
|
|
326
|
+
{ if: { properties: { operation: { const: "pause" } }, required: ["operation"] }, then: { required: ["endpoint_name"] } },
|
|
327
|
+
{ if: { properties: { operation: { const: "resume" } }, required: ["operation"] }, then: { required: ["endpoint_name"] } },
|
|
328
|
+
{ if: { properties: { operation: { const: "history" } }, required: ["operation"] }, then: { required: ["endpoint_name"] } },
|
|
329
|
+
],
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
{
|
|
334
|
+
name: "manage_webhooks",
|
|
335
|
+
description: `Observe + control HTTP-trigger webhooks at runtime, by endpoint name.
|
|
336
|
+
|
|
337
|
+
Webhooks are DECLARED in dypai/endpoints/<name>.yaml under \`trigger.webhook\`.
|
|
338
|
+
This tool exists for runtime observability and control WITHOUT round-tripping
|
|
339
|
+
the YAML.
|
|
340
|
+
|
|
341
|
+
All per-webhook operations target an endpoint by its \`endpoint_name\` — the
|
|
342
|
+
same name you wrote in the YAML. Internal UUIDs are resolved automatically.
|
|
343
|
+
|
|
344
|
+
Operations:
|
|
345
|
+
- list: All webhooks. Each item includes the FULL URL (ready to paste
|
|
346
|
+
into Stripe / Telegram / Polar / etc. provider config).
|
|
347
|
+
- get: Full detail of one webhook (URL, last_called_at, total_calls).
|
|
348
|
+
- pause: Set is_active=false (incoming calls are rejected).
|
|
349
|
+
- resume: Set is_active=true.
|
|
350
|
+
- history: Recent incoming calls (status, payload preview, errors).
|
|
351
|
+
- test: Fire a synthetic payload at the webhook to validate the trigger.
|
|
352
|
+
Optional: test_payload (object), test_headers (object).
|
|
353
|
+
- rotate_secret: Generate a new HMAC secret. Use after a leak; you'll need
|
|
354
|
+
to update the secret in the upstream provider too.
|
|
355
|
+
|
|
356
|
+
To CHANGE the path, auth mode, allowed methods, or filtering: edit the
|
|
357
|
+
endpoint YAML and \`dypai_push\`. This tool does NOT modify the definition.`,
|
|
358
|
+
inputSchema: {
|
|
359
|
+
type: "object",
|
|
360
|
+
properties: {
|
|
361
|
+
project_id: { type: "string", description: "Project UUID. Required for user tokens; auto-detected for project tokens." },
|
|
362
|
+
operation: { type: "string", enum: ["list", "get", "pause", "resume", "history", "test", "rotate_secret"] },
|
|
363
|
+
endpoint_name: { type: "string", description: "Endpoint name as declared in dypai/endpoints/<name>.yaml. Required for: get, pause, resume, history, test, rotate_secret." },
|
|
364
|
+
is_active: { type: "boolean", description: "Filter by active state. Optional for: list." },
|
|
365
|
+
limit: { type: "integer", description: "Max results. list: 1-200 (default 50). history: 1-100 (default 20)." },
|
|
366
|
+
offset: { type: "integer", description: "Pagination offset. Optional for: list, history. Default 0." },
|
|
367
|
+
test_payload: { type: "object", description: "Synthetic JSON body to send. Optional for: test (default: empty object)." },
|
|
368
|
+
test_headers: { type: "object", description: "Extra headers to attach to the synthetic call. Optional for: test." },
|
|
369
|
+
},
|
|
370
|
+
required: ["operation"],
|
|
371
|
+
allOf: [
|
|
372
|
+
{ if: { properties: { operation: { const: "get" } }, required: ["operation"] }, then: { required: ["endpoint_name"] } },
|
|
373
|
+
{ if: { properties: { operation: { const: "pause" } }, required: ["operation"] }, then: { required: ["endpoint_name"] } },
|
|
374
|
+
{ if: { properties: { operation: { const: "resume" } }, required: ["operation"] }, then: { required: ["endpoint_name"] } },
|
|
375
|
+
{ if: { properties: { operation: { const: "history" } }, required: ["operation"] }, then: { required: ["endpoint_name"] } },
|
|
376
|
+
{ if: { properties: { operation: { const: "test" } }, required: ["operation"] }, then: { required: ["endpoint_name"] } },
|
|
377
|
+
{ if: { properties: { operation: { const: "rotate_secret" } }, required: ["operation"] }, then: { required: ["endpoint_name"] } },
|
|
378
|
+
],
|
|
379
|
+
},
|
|
380
|
+
},
|
|
141
381
|
|
|
142
382
|
// ── Knowledge ─────────────────────────────────────────────────────────────
|
|
143
383
|
{ name: "search_docs", description: "Search DYPAI documentation. Use this when unsure about SDK usage, auth patterns, workflow nodes, or platform features. Returns relevant documentation chunks.", inputSchema: { type: "object", properties: { query: { type: "string", description: "What you want to learn about" } }, required: ["query"] } },
|
|
@@ -149,11 +389,25 @@ const REMOTE_TOOLS = [
|
|
|
149
389
|
|
|
150
390
|
const SERVER_INSTRUCTIONS = `You are building full-stack applications on the DYPAI platform. You handle backend (endpoints, database, auth, realtime) AND frontend (SDK integration, UI code).
|
|
151
391
|
|
|
392
|
+
## Creating a new project — template-first
|
|
393
|
+
|
|
394
|
+
When the user asks to CREATE a new project (not work on an existing one), you MUST check for a matching project template before falling back to a blank one:
|
|
395
|
+
|
|
396
|
+
1. **Infer the intent.** If the user's request describes a recognizable app type — clinic / gym / waitlist / SaaS dashboard / e-commerce / landing / CRM / marketplace / chat app / booking / etc. — there's probably a pre-built starter for it.
|
|
397
|
+
2. **Call \`search_project_templates(query: "...")\`** with a natural description of what they want. Use a few keywords, not a full sentence. Returns templates with a \`slug\`, name, description, and category.
|
|
398
|
+
3. **If a template looks like a reasonable fit**, confirm briefly with the user ("I found a 'clinic' starter that includes patients, appointments and billing — start from that?") and then call \`create_project(name, template_slug: "<slug>")\`.
|
|
399
|
+
4. **Only if nothing fits**, call \`create_project(name)\` without \`template_slug\` (or with \`template_slug: "blank"\`). Starting blank means writing every endpoint, table, and UI from scratch — skipping a template that covers 70% of the work costs the user an hour of rebuilding boilerplate.
|
|
400
|
+
|
|
401
|
+
Trigger phrases that should make you reach for \`search_project_templates\` first: "create an app for X", "build me a Y", "I want to launch a Z", "start a new project for…", "montar una app de…", "I need a dashboard that…".
|
|
402
|
+
|
|
403
|
+
Do NOT silently default to a blank project. If the match is uncertain, ASK the user before creating.
|
|
404
|
+
|
|
152
405
|
## Getting Started
|
|
153
406
|
1. Call list_projects() to find your project_id (skip if you already know it).
|
|
154
|
-
2. **ALWAYS CALL dypai_pull FIRST** on any project you haven't worked on in this session. It materializes ./dypai/ (endpoints, SQL, prompts, code, schema.sql, node-catalog.json, realtime.yaml) and returns an \`overview\` block with endpoint groups, credentials, and tool-enabled endpoints — everything you need to orient yourself in one call.
|
|
155
|
-
3.
|
|
156
|
-
4.
|
|
407
|
+
2. **ALWAYS CALL dypai_pull FIRST** on any project you haven't worked on in this session. It materializes ./dypai/ (endpoints, SQL, prompts, code, schema.sql, node-catalog.json, realtime.yaml) and returns an \`overview\` block with endpoint groups, credentials, and tool-enabled endpoints — everything you need to orient yourself in one call. This ONLY brings BACKEND state.
|
|
408
|
+
3. If you're going to read or edit FRONTEND code (React / Vite / Next / etc.) and the source isn't already on this machine, **also call \`manage_frontend(operation: "sync", targetDirectory: <absolute path>)\`** to download it. \`dypai_pull\` and \`manage_frontend(sync)\` are independent: run both when starting fresh on a full-stack project.
|
|
409
|
+
4. BEFORE implementing an unfamiliar feature, call search_docs with the topic (e.g. "auth", "upload files", "realtime", "stripe").
|
|
410
|
+
5. Build backend first, then frontend.
|
|
157
411
|
|
|
158
412
|
## Build Backend (git-first workflow)
|
|
159
413
|
Everything about endpoints lives in files under ./dypai/ — you NEVER call endpoint CRUD tools directly. There is no create_endpoint / update_endpoint / add_node in this MCP anymore.
|
|
@@ -170,7 +424,7 @@ Everything about endpoints lives in files under ./dypai/ — you NEVER call endp
|
|
|
170
424
|
|
|
171
425
|
## Node reference — read the local catalog BEFORE picking
|
|
172
426
|
|
|
173
|
-
\`dypai/node-catalog.json\` is the single source of truth for every node_type this engine exposes, with its full input/output schema. It's cached locally by \`dypai_pull\` and auto-refreshed. **Read it directly with your Read tool** — don't
|
|
427
|
+
\`dypai/node-catalog.json\` is the single source of truth for every node_type this engine exposes, with its full input/output schema. It's cached locally by \`dypai_pull\` and auto-refreshed. **Read it directly with your Read tool** — don't guess parameter names, don't invent node_types. If a type isn't in node-catalog.json, it doesn't exist.
|
|
174
428
|
|
|
175
429
|
Typical flow when adding a step to a workflow:
|
|
176
430
|
1. Decide the CONCEPT (DB insert? HTTP call? shape fields? branching?).
|
|
@@ -178,8 +432,6 @@ Typical flow when adding a step to a workflow:
|
|
|
178
432
|
3. If you find one: read its input_schema from the same file and configure the node.
|
|
179
433
|
4. If nothing native fits naturally: consider \`javascript_code\` / \`python_code\` (see below).
|
|
180
434
|
|
|
181
|
-
\`search_nodes(query="...")\` is a semantic/vector fallback for when the intent is vague AND you can't locate it by name in node-catalog.json. It's rarely needed once you have the file open.
|
|
182
|
-
|
|
183
435
|
## Node selection — think before choosing
|
|
184
436
|
|
|
185
437
|
javascript_code is a fine tool. The problem is picking it reflexively without considering native alternatives. Native nodes are validated by dypai_validate, observable per-node in traces, and readable in PR diffs — so when one fits, it's the better default. JS wins when the logic is genuinely custom and bundling multiple concerns is clearer than chaining 4+ native nodes.
|
|
@@ -219,6 +471,78 @@ When you hit a red flag, replace the JS node. When the JS legitimately does more
|
|
|
219
471
|
- Client-side: \`dypai.realtime.subscribe(table, { filter, event }, callback)\`.
|
|
220
472
|
- See search_docs "realtime" for patterns.
|
|
221
473
|
|
|
474
|
+
## Agent tools (endpoints as tools for an \`agent\` node)
|
|
475
|
+
|
|
476
|
+
Any endpoint can be flagged as a **tool** so that \`agent\` nodes elsewhere in the project can invoke it. This is how you give an LLM the ability to query your DB, call external APIs, send emails, etc. — you expose the action as a normal endpoint and mark it \`tool: true\`.
|
|
477
|
+
|
|
478
|
+
### Step 1 — mark the endpoint as a tool (in its YAML)
|
|
479
|
+
|
|
480
|
+
\`\`\`yaml
|
|
481
|
+
name: search-products
|
|
482
|
+
method: GET
|
|
483
|
+
tool: true # ← exposes it to agent nodes
|
|
484
|
+
tool_description: | # ← what the agent should know about this tool
|
|
485
|
+
Search the product catalog by name or category.
|
|
486
|
+
Use when the user asks about availability, prices, or features.
|
|
487
|
+
input: # ← define a tight schema; the LLM reads it
|
|
488
|
+
type: object
|
|
489
|
+
required: [query]
|
|
490
|
+
properties:
|
|
491
|
+
query: { type: string, description: "Free-text search term" }
|
|
492
|
+
limit: { type: integer, default: 10 }
|
|
493
|
+
trigger:
|
|
494
|
+
http_api:
|
|
495
|
+
auth_mode: api_key # agent calls are server-to-server
|
|
496
|
+
workflow:
|
|
497
|
+
nodes:
|
|
498
|
+
- id: run
|
|
499
|
+
type: dypai_database
|
|
500
|
+
operation: query
|
|
501
|
+
return: true
|
|
502
|
+
query: SELECT id, name, price, stock FROM products WHERE name ILIKE '%' || \${input.query} || '%' LIMIT \${input.limit}
|
|
503
|
+
\`\`\`
|
|
504
|
+
|
|
505
|
+
Rules:
|
|
506
|
+
- A tool endpoint is still a regular endpoint (same trigger, same workflow, same deploy). \`tool: true\` is additive.
|
|
507
|
+
- **\`tool_description\` is critical** — this is the prompt the LLM sees when deciding whether to call your tool. Be specific about *when* to use it and *what it returns*. Vague descriptions cause the agent to skip useful tools or misuse them.
|
|
508
|
+
- Define a **tight \`input\` JSON Schema** with \`description\` on each field. The LLM uses the schema to fabricate arguments; sloppy schemas = sloppy calls.
|
|
509
|
+
- Auth mode: usually \`api_key\` (the agent node calls server-side) or \`jwt\` if you need \${current_user_id} in the tool. **Never \`public\`** for tools that write data.
|
|
510
|
+
|
|
511
|
+
### Step 2 — wire the tool into an \`agent\` node (in another endpoint's YAML)
|
|
512
|
+
|
|
513
|
+
In the YAML you reference tools **by name**, not by UUID. The codec converts names ↔ UUIDs automatically on push/pull.
|
|
514
|
+
|
|
515
|
+
\`\`\`yaml
|
|
516
|
+
workflow:
|
|
517
|
+
nodes:
|
|
518
|
+
- id: chat
|
|
519
|
+
type: agent
|
|
520
|
+
provider: openai # or anthropic | google
|
|
521
|
+
model: gpt-4o
|
|
522
|
+
credential: openai-prod # must exist — use get_app_credentials
|
|
523
|
+
system_prompt: |
|
|
524
|
+
You are a shop assistant. Use the search-products tool when the user
|
|
525
|
+
asks about availability, prices, or categories.
|
|
526
|
+
input: \${input.message}
|
|
527
|
+
tools: [search-products, send-email] # ← by name. Codec resolves to UUIDs on push.
|
|
528
|
+
max_iterations: 5
|
|
529
|
+
return: true
|
|
530
|
+
\`\`\`
|
|
531
|
+
|
|
532
|
+
**COMMON TRAP**: the raw engine parameter is called \`tool_ids\` (takes UUIDs). If you read \`dypai/node-catalog.json\` you'll see that name. In YAML you MUST write \`tools: [names]\` — if you write \`tool_ids: ["my-endpoint"]\` the codec does NOT transform it, the engine receives "my-endpoint" as a literal UUID, and you fail silently in production. Always \`tools: [names]\` in YAML.
|
|
533
|
+
|
|
534
|
+
### Step 3 — discover existing tools
|
|
535
|
+
|
|
536
|
+
\`dypai_pull\` returns an \`overview.endpoints.tool_endpoints\` array listing every endpoint marked \`tool: true\` with its \`tool_description\`. Check that list before writing a new tool — you might already have it.
|
|
537
|
+
|
|
538
|
+
### Validation
|
|
539
|
+
|
|
540
|
+
\`dypai_validate\` has rule \`agent_tool_not_found\`: if an \`agent\` node references \`tools: ["foo"]\` and either no endpoint named "foo" exists, or it's not marked \`tool: true\`, validate fails with a fix_hint listing the available tool endpoints. Catches typos before you push.
|
|
541
|
+
|
|
542
|
+
### Recursion safety
|
|
543
|
+
|
|
544
|
+
The engine enforces a depth limit (currently 3) on agent→tool→agent chains, plus per-workflow stack tracking. A tool can itself be an agent endpoint, but runaway loops are cut off automatically.
|
|
545
|
+
|
|
222
546
|
## Testing & Debugging
|
|
223
547
|
|
|
224
548
|
- **"Let me run this endpoint and see"** → \`dypai_test_endpoint\`
|
|
@@ -255,6 +579,22 @@ When you hit a red flag, replace the JS node. When the JS legitimately does more
|
|
|
255
579
|
- Do NOT call the API directly with fetch() — always use the SDK
|
|
256
580
|
- Do NOT create auth endpoints — auth is built-in via SDK (dypai.auth.*)
|
|
257
581
|
|
|
582
|
+
### \`.env\` file — required for the SDK to reach the engine
|
|
583
|
+
The frontend SDK reads the engine URL from an environment variable. \`.env\` is gitignored, so \`manage_frontend(sync)\` does NOT bring it down. **If the project has no \`.env\` after sync (the response sets \`env_file_missing: true\`), you MUST create it** before running or deploying the frontend:
|
|
584
|
+
|
|
585
|
+
\`\`\`bash
|
|
586
|
+
# Vite / React / Svelte / Vue / etc.
|
|
587
|
+
VITE_DYPAI_URL=https://<project_id>.dypai.app
|
|
588
|
+
VITE_PROJECT_ID=<project_id>
|
|
589
|
+
|
|
590
|
+
# Next.js (prefix matters — must be NEXT_PUBLIC_ to be readable in the browser)
|
|
591
|
+
NEXT_PUBLIC_DYPAI_URL=https://<project_id>.dypai.app
|
|
592
|
+
\`\`\`
|
|
593
|
+
|
|
594
|
+
- \`<project_id>\` is the UUID from \`list_projects\` / \`dypai.config.yaml\`.
|
|
595
|
+
- Override the base domain with the \`DYPAI_ENGINE_BASE\` env var (default \`dypai.app\`) only for self-hosted setups.
|
|
596
|
+
- Without \`.env\`, every SDK call fails with a network error / 404 because \`createClient(undefined)\` resolves nowhere. Don't try to debug the code — it's the missing env var.
|
|
597
|
+
|
|
258
598
|
## Deploy Frontend
|
|
259
599
|
- \`manage_frontend(operation: deploy, sourceDirectory, project_id)\` — uploads source, returns immediately with build_status=queued.
|
|
260
600
|
- Poll \`manage_frontend(operation: build_status)\` every ~5s until status is "success" or "failure" (typical build: 20-60s).
|
|
@@ -276,6 +616,97 @@ When you hit a red flag, replace the JS node. When the JS legitimately does more
|
|
|
276
616
|
- Placeholders: \${input.<field>}, \${nodes.<node_id>.<field>}, \${current_user_id}, \${current_user_role}.
|
|
277
617
|
- \${current_user_id} / \${current_user_role} only work with jwt auth mode.
|
|
278
618
|
|
|
619
|
+
## dypai_database — choose between query and mutation
|
|
620
|
+
|
|
621
|
+
The dypai_database node has TWO operations. Pick the right one and you'll never
|
|
622
|
+
need the others:
|
|
623
|
+
|
|
624
|
+
### \`operation: query\` — SQL (the power tool, no limits)
|
|
625
|
+
Use for ANY non-trivial read or any complex write. Joins, CTEs, subqueries,
|
|
626
|
+
window functions, FILTER clauses, aggregations, multi-table — all fair game.
|
|
627
|
+
|
|
628
|
+
\`\`\`yaml
|
|
629
|
+
- id: dashboard
|
|
630
|
+
type: dypai_database
|
|
631
|
+
operation: query
|
|
632
|
+
return: true
|
|
633
|
+
query: |
|
|
634
|
+
WITH revenue AS (
|
|
635
|
+
SELECT SUM(amount) AS total FROM invoices
|
|
636
|
+
WHERE user_id = \${current_user_id} AND paid_at >= date_trunc('month', NOW())
|
|
637
|
+
)
|
|
638
|
+
SELECT
|
|
639
|
+
(SELECT total FROM revenue) AS revenue,
|
|
640
|
+
(SELECT COUNT(*) FROM tasks WHERE user_id = \${current_user_id} AND status = 'pending') AS pending_tasks
|
|
641
|
+
\`\`\`
|
|
642
|
+
|
|
643
|
+
### \`operation: mutation\` — declarative CRUD (no SQL needed)
|
|
644
|
+
Use for trivial single-table writes. The shape decides the operation:
|
|
645
|
+
|
|
646
|
+
\`\`\`yaml
|
|
647
|
+
# INSERT
|
|
648
|
+
- id: create
|
|
649
|
+
type: dypai_database
|
|
650
|
+
operation: mutation
|
|
651
|
+
return: true
|
|
652
|
+
table: clients
|
|
653
|
+
insert: { name: \${input.name}, user_id: \${current_user_id} }
|
|
654
|
+
|
|
655
|
+
# UPSERT (insert + on_conflict)
|
|
656
|
+
- id: upsert
|
|
657
|
+
type: dypai_database
|
|
658
|
+
operation: mutation
|
|
659
|
+
return: true
|
|
660
|
+
table: clients
|
|
661
|
+
insert: { id: \${input.id}, name: \${input.name} }
|
|
662
|
+
on_conflict: { columns: [id], action: update }
|
|
663
|
+
|
|
664
|
+
# UPDATE
|
|
665
|
+
- id: update
|
|
666
|
+
type: dypai_database
|
|
667
|
+
operation: mutation
|
|
668
|
+
return: true
|
|
669
|
+
table: clients
|
|
670
|
+
update: { name: \${input.name} }
|
|
671
|
+
where: { id: \${input.id}, user_id: \${current_user_id} }
|
|
672
|
+
|
|
673
|
+
# DELETE
|
|
674
|
+
- id: delete
|
|
675
|
+
type: dypai_database
|
|
676
|
+
operation: mutation
|
|
677
|
+
table: clients
|
|
678
|
+
delete: true
|
|
679
|
+
where: { id: \${input.id}, user_id: \${current_user_id} }
|
|
680
|
+
\`\`\`
|
|
681
|
+
|
|
682
|
+
### Decision tree (use this — it's all you need)
|
|
683
|
+
- Reading anything → \`operation: query\` (yes, even simple SELECTs — query is fine)
|
|
684
|
+
- Writing one table, no fancy SQL → \`operation: mutation\`
|
|
685
|
+
- Anything else (joins, subqueries, CTEs, multi-table writes) → \`operation: query\`
|
|
686
|
+
|
|
687
|
+
### Legacy operations (deprecated but still work)
|
|
688
|
+
\`select\`, \`insert\`, \`update\`, \`delete\`, \`upsert\`, \`aggregate\`, \`copy_to\`, \`custom_query\` — old endpoints in production keep running. Don't write new ones with these. \`dypai_validate\` warns if you do.
|
|
689
|
+
|
|
690
|
+
## SQL Placeholders & Auto-Cast (IMPORTANT)
|
|
691
|
+
Inside SQL queries (operation: query, or mutation values) the engine binds
|
|
692
|
+
placeholders as real Postgres parameters (\$1, \$2…) — NOT string interpolation.
|
|
693
|
+
This is injection-safe and type-safe by construction.
|
|
694
|
+
|
|
695
|
+
The engine auto-casts based on the value SHAPE:
|
|
696
|
+
UUID-shaped string → bound as \$1::uuid (e.g. \${current_user_id})
|
|
697
|
+
plain object/array → bound as \$1::jsonb (e.g. \${input.metadata})
|
|
698
|
+
Date instance → bound as \$1::timestamptz
|
|
699
|
+
text / int / bool → bound without cast (Postgres infers from column)
|
|
700
|
+
|
|
701
|
+
**Write SQL naturally — DO NOT add manual casts**:
|
|
702
|
+
✅ WHERE user_id = \${current_user_id}
|
|
703
|
+
❌ WHERE user_id = '\${current_user_id}'::uuid (redundant, validator warns)
|
|
704
|
+
|
|
705
|
+
✅ INSERT INTO events(payload) VALUES (\${input.meta})
|
|
706
|
+
❌ INSERT INTO events(payload) VALUES ('\${input.meta}'::jsonb) (redundant)
|
|
707
|
+
|
|
708
|
+
The validator (dypai_validate) flags manual casts with rule \`sql_redundant_uuid_cast\`.
|
|
709
|
+
|
|
279
710
|
## Common Mistakes
|
|
280
711
|
- Do NOT default to javascript_code — use native nodes (see decision tree above).
|
|
281
712
|
- Do NOT create auth endpoints — auth is built-in via SDK (dypai.auth.*).
|
|
@@ -285,6 +716,86 @@ When you hit a red flag, replace the JS node. When the JS legitimately does more
|
|
|
285
716
|
- Do NOT push without dypai_diff — always preview first.
|
|
286
717
|
- Do NOT look up tables / endpoints remotely when a local dypai/ exists. Use Read/Glob/Grep on the files.
|
|
287
718
|
- Do NOT push just to test a change — use dypai_test_endpoint to run the LOCAL edited version against the engine. Only push when you're satisfied.
|
|
719
|
+
|
|
720
|
+
## Canonical YAML format (READ CAREFULLY — typical confusion points)
|
|
721
|
+
|
|
722
|
+
Endpoint YAMLs follow ONE canonical shape. The codec tolerates some legacy aliases for forward-compat, but the canonical form is what you should write. These are the patterns where agents most commonly slip:
|
|
723
|
+
|
|
724
|
+
### Trigger is TOP-LEVEL, never a node
|
|
725
|
+
\`\`\`yaml
|
|
726
|
+
# ✅ CORRECT
|
|
727
|
+
trigger:
|
|
728
|
+
http_api:
|
|
729
|
+
auth_mode: jwt
|
|
730
|
+
workflow:
|
|
731
|
+
nodes:
|
|
732
|
+
- id: list
|
|
733
|
+
type: dypai_database
|
|
734
|
+
...
|
|
735
|
+
\`\`\`
|
|
736
|
+
\`\`\`yaml
|
|
737
|
+
# ❌ WRONG — start_trigger is NOT a node anymore
|
|
738
|
+
workflow:
|
|
739
|
+
nodes:
|
|
740
|
+
- id: start
|
|
741
|
+
type: start_trigger # the codec auto-strips this and warns
|
|
742
|
+
...
|
|
743
|
+
\`\`\`
|
|
744
|
+
Trigger options: \`http_api\` / \`webhook\` / \`schedule\` / \`telegram\`. \`auth_mode\` is one of \`jwt\` / \`api_key\` / \`public\`.
|
|
745
|
+
|
|
746
|
+
### Nodes use \`type\`, not \`node_type\`
|
|
747
|
+
\`\`\`yaml
|
|
748
|
+
- id: insert
|
|
749
|
+
type: dypai_database # ✅ canonical
|
|
750
|
+
return: true # ✅ not is_return
|
|
751
|
+
\`\`\`
|
|
752
|
+
The codec accepts \`node_type\` and \`is_return\` for safety, but write the canonical form.
|
|
753
|
+
|
|
754
|
+
### Edges use \`from/to\`, not \`source/target\`
|
|
755
|
+
\`\`\`yaml
|
|
756
|
+
edges:
|
|
757
|
+
- { from: a, to: b } # ✅ canonical
|
|
758
|
+
- { from: cond, to: y, source_handle: "true" } # ✅ for branching
|
|
759
|
+
\`\`\`
|
|
760
|
+
|
|
761
|
+
### Parameters are FLAT inline on the node, not nested
|
|
762
|
+
\`\`\`yaml
|
|
763
|
+
# ✅ CORRECT — params flat on the node
|
|
764
|
+
- id: insert
|
|
765
|
+
type: dypai_database
|
|
766
|
+
operation: insert
|
|
767
|
+
table: orders
|
|
768
|
+
data: { user_id: \${current_user_id}, qty: \${input.qty} }
|
|
769
|
+
\`\`\`
|
|
770
|
+
\`\`\`yaml
|
|
771
|
+
# ❌ AVOID — only use nested parameters: when a param NAME collides with id/type/return/variable
|
|
772
|
+
- id: insert
|
|
773
|
+
type: dypai_database
|
|
774
|
+
parameters:
|
|
775
|
+
operation: insert
|
|
776
|
+
table: orders
|
|
777
|
+
\`\`\`
|
|
778
|
+
|
|
779
|
+
### Multiple return paths are fine
|
|
780
|
+
Mark \`return: true\` on every node that produces a final HTTP response. Workflow execution only reaches one of them per run.
|
|
781
|
+
|
|
782
|
+
### Agent tools use \`tool:\` + \`tools: [names]\`
|
|
783
|
+
\`\`\`yaml
|
|
784
|
+
# Endpoint exposes itself as a tool:
|
|
785
|
+
name: search-products
|
|
786
|
+
tool: true
|
|
787
|
+
tool_description: "Search the product catalog. Use for availability/prices."
|
|
788
|
+
\`\`\`
|
|
789
|
+
\`\`\`yaml
|
|
790
|
+
# Agent node references tools by NAME (codec resolves to UUIDs on push):
|
|
791
|
+
- id: chat
|
|
792
|
+
type: agent
|
|
793
|
+
tools: [search-products, send-email] # ✅ names
|
|
794
|
+
\`\`\`
|
|
795
|
+
⚠️ The engine param is \`tool_ids\` (UUIDs) but in YAML you write \`tools: [names]\`. Writing \`tool_ids: ["name"]\` in YAML bypasses the codec transform and fails in production. See the "Agent tools" section above for the full pattern.
|
|
796
|
+
|
|
797
|
+
### When in doubt
|
|
798
|
+
Read \`dypai/endpoints/_example.yaml.disabled\` (auto-shipped on empty projects) — it's a complete tour of every pattern.
|
|
288
799
|
`
|
|
289
800
|
|
|
290
801
|
// ── MCP Protocol (JSON-RPC over stdio) ──────────────────────────────────────
|
|
@@ -306,7 +817,7 @@ async function handleRequest(msg) {
|
|
|
306
817
|
return makeResponse(id, {
|
|
307
818
|
protocolVersion: "2024-11-05",
|
|
308
819
|
capabilities: { tools: {} },
|
|
309
|
-
serverInfo: { name: "dypai", version: "1.
|
|
820
|
+
serverInfo: { name: "dypai", version: "1.3.0" },
|
|
310
821
|
instructions: SERVER_INSTRUCTIONS,
|
|
311
822
|
})
|
|
312
823
|
}
|
|
@@ -390,8 +901,17 @@ async function handleRequest(msg) {
|
|
|
390
901
|
content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }],
|
|
391
902
|
})
|
|
392
903
|
} catch (e) {
|
|
904
|
+
// Return a structured error so the agent can parse `success`/`error`
|
|
905
|
+
// consistently with our other "soft failure" responses (validate,
|
|
906
|
+
// test_endpoint, etc.). Still flag isError=true at the MCP level.
|
|
907
|
+
const payload = {
|
|
908
|
+
success: false,
|
|
909
|
+
error: e.message || String(e),
|
|
910
|
+
// First few stack frames help debug which file blew up.
|
|
911
|
+
stack: (e.stack || "").split("\n").slice(0, 4).join("\n"),
|
|
912
|
+
}
|
|
393
913
|
return makeResponse(id, {
|
|
394
|
-
content: [{ type: "text", text:
|
|
914
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
395
915
|
isError: true,
|
|
396
916
|
})
|
|
397
917
|
}
|