@ductape/mcp 0.1.13 → 0.1.15

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.
Files changed (3) hide show
  1. package/dist/index.js +1259 -4
  2. package/package.json +1 -1
  3. package/src/index.ts +1291 -4
package/src/index.ts CHANGED
@@ -166,7 +166,9 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
166
166
  webhooks.events.list [app_tag, webhook_tag]
167
167
 
168
168
  ━━━ MODULE: sessions ━━━
169
- sessions.create [product_tag, data: { tag: string, name: string, description?: string, expiry: number, period: "seconds"|"minutes"|"hours"|"days", selector: string, schema: { fieldName: "string"|"number"|"boolean" } }]
169
+ sessions.create [product_tag, data: { tag: string, name: string, description?: string, expiry: number, period: "seconds"|"minutes"|"hours"|"days", selector: string, schema: { fieldName: { type: "string"|"number"|"boolean"|"object"|"array", required?: boolean, description?: string } } }]
170
+ ← selector is the dot-path of the primary identifier field (e.g. "playerId"); it MUST exist in schema.
171
+ ← schema declares the shape of data encrypted in the JWT; actual values are passed at runtime via sessions.start.
170
172
  sessions.update [product_tag, session_tag, data: { name?: string, description?: string, expiry?: number, period?: "seconds"|"minutes"|"hours"|"days", selector?: string, schema?: object }]
171
173
  sessions.fetch [product_tag, session_tag]
172
174
  sessions.list [product_tag]
@@ -302,8 +304,15 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
302
304
  messageBrokers.fetch [product_tag, broker_tag]
303
305
  messageBrokers.list [product_tag]
304
306
  messageBrokers.delete [product_tag, broker_tag]
305
- messageBrokers.topics.create [product_tag, data: { tag: string, name: string, broker: string, type: "producer"|"consumer"|"both" }]
306
- messageBrokers.topics.update [product_tag, topic_tag, data: { name?: string, type?: "producer"|"consumer"|"both" }]
307
+ messageBrokers.topics.create [product_tag, data: { tag: string, name: string, broker: string,
308
+ description?: string, sample?: object, idempotent?: boolean,
309
+ queueUrls?: [{ env_slug: string, url: string }] // SQS only: per-env queue URL per topic
310
+ }]
311
+ ← A broker can have unlimited topics. Add one per logical event type.
312
+ ← For SQS: each topic needs queueUrls mapping env slug → the SQS queue URL for that topic+env.
313
+ ← For Pub/Sub and Service Bus: topic name resolves from the tag or broker env config; no queueUrls needed.
314
+ messageBrokers.topics.update [product_tag, topic_tag, data: { name?: string, description?: string,
315
+ sample?: object, idempotent?: boolean, queueUrls?: [{ env_slug: string, url: string }] }]
307
316
  messageBrokers.topics.fetch [product_tag, topic_tag]
308
317
  messageBrokers.topics.list [product_tag, broker_tag]
309
318
  messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value }, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="messaging", method="produce", targets={broker, event})
@@ -383,6 +392,15 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
383
392
  databases.action.delete [action_tag]
384
393
  databases.action.dispatch [{ product, env, database, action, input, schedule? }] ← CALL ductape_generate_payload FIRST (operation_family="database", method="dispatch", targets={database, action})
385
394
  databases.dispatch [{ product, env, database, action, input, schedule? }] ← CALL ductape_generate_payload FIRST (operation_family="database", method="dispatch")
395
+ databases.beginTransaction [{ product, env, database, isolationLevel?: "READ_COMMITTED"|"REPEATABLE_READ"|"SERIALIZABLE" }]
396
+ → returns a transaction object; pass it to insert/update/delete/upsert/query calls as the last argument.
397
+ Commit with: transaction.commit() Rollback with: transaction.rollback()
398
+ BEFORE using transactions: ask the user which database type and tier they are running.
399
+ Transaction support varies by database — call ductape_docs({ topic: "transactions" }) for the full matrix.
400
+ databases.transaction [{ product, env, database, isolationLevel? }, async (tx) => { … }]
401
+ → managed transaction: commits automatically on success, rolls back on error.
402
+ Prefer this over beginTransaction when the callback is self-contained.
403
+ See ductape_docs({ topic: "transactions" }) for database/tier compatibility before proceeding.
386
404
 
387
405
  ━━━ MODULE: graph ━━━
388
406
  graph.create [{ product, tag, name, description?, type: "neo4j"|"nebula"|"arangodb", envs: [{slug, connection_url, username?, password?}] }]
@@ -586,7 +604,7 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
586
604
  caches.list [product_tag]
587
605
  caches.delete [product_tag, cache_tag]
588
606
  caches.get [{ key: string }]
589
- caches.set [{ product, cache, key, value: string, componentTag?, componentType?, expiry?: Date }]
607
+ caches.set [{ product, cache, key, value: string, componentTag?, componentType?, expiry?: string (ISO 8601 absolute timestamp — NOT a duration; e.g. new Date(Date.now()+3600000).toISOString() for 1h TTL), env }]
590
608
  caches.clear [{ key: string }]
591
609
  caches.clearAll [{ product, cache, env? }]
592
610
  caches.fetchValues [{ product, cache, env?, page?, limit?, expiryFilter?: "all"|"expiring"|"permanent"|"expired" }]
@@ -959,6 +977,1232 @@ function runCli(command: string): { success: boolean; output: string } {
959
977
  }
960
978
  }
961
979
 
980
+ const docsInputSchema = z.object({
981
+ topic: z.string().describe(
982
+ 'Feature topic to look up. Supported: ' +
983
+ 'transactions, presave, triggers, aggregations, migrations, indexes, performance, actions, ' +
984
+ 'graphs, storage, cloud, vector, warehouse, secrets, apps, products, sessions, caches, ' +
985
+ 'notifications, resilience, features, events, logs',
986
+ ),
987
+ });
988
+
989
+ const DOCS: Record<string, string> = {
990
+ transactions: `
991
+ DUCTAPE DATABASE TRANSACTIONS
992
+
993
+ Ask the user which database type and tier they are using before advising on transaction support.
994
+
995
+ Support matrix:
996
+ PostgreSQL — full ACID; all isolation levels (READ_UNCOMMITTED, READ_COMMITTED,
997
+ REPEATABLE_READ, SERIALIZABLE). Always supported.
998
+ MySQL — full ACID; same isolation levels as PostgreSQL.
999
+ MongoDB — multi-document transactions require a replica set.
1000
+ Atlas M10+ (dedicated): supported.
1001
+ Atlas M0/M2/M5 (free/shared): NOT supported — all writes are
1002
+ single-document atomic only. Confirm tier with user before proceeding.
1003
+ Self-hosted: supported if replica set mode is enabled.
1004
+ Cassandra — does NOT support ACID transactions. Lightweight transactions (LWT)
1005
+ only for single-partition compare-and-set.
1006
+ DynamoDB — transactional API supported (TransactWrite) but NOT via Ductape's
1007
+ transaction abstraction; use single operations with condition expressions.
1008
+
1009
+ Usage patterns:
1010
+ // Managed (preferred) — auto-commit on success, auto-rollback on error:
1011
+ await ductape.database.transaction(
1012
+ { product, env, database, isolationLevel: 'READ_COMMITTED' },
1013
+ async (tx) => {
1014
+ await ductape.database.insert({ ..., transaction: tx });
1015
+ await ductape.database.update({ ..., transaction: tx });
1016
+ }
1017
+ );
1018
+
1019
+ // Manual — commit/rollback yourself:
1020
+ const tx = await ductape.database.beginTransaction({ product, env, database });
1021
+ try {
1022
+ await ductape.database.insert({ ..., transaction: tx });
1023
+ await tx.commit();
1024
+ } catch (e) {
1025
+ await tx.rollback();
1026
+ throw e;
1027
+ }
1028
+
1029
+ Via ductape_execute:
1030
+ ductape_execute("databases.beginTransaction", [{ product, env, database, isolationLevel? }])
1031
+ → returns a transaction reference you can pass to subsequent insert/update/delete calls.
1032
+ `.trim(),
1033
+
1034
+ presave: `
1035
+ DUCTAPE PRE-SAVE HOOKS
1036
+
1037
+ Pre-save operations transform or validate data before it is written to the database.
1038
+ Pass a preSave array to insert or update operations. Operations run in priority order.
1039
+
1040
+ Available operations (PreSaveOperationType):
1041
+ encrypt — AES-256 encrypt field (use for PII, secrets)
1042
+ hash — one-way hash (bcrypt/argon2 for passwords)
1043
+ mask — mask sensitive data (e.g. **** **** **** 1234)
1044
+ trim — strip leading/trailing whitespace
1045
+ lowercase — convert string to lowercase
1046
+ uppercase — convert string to uppercase
1047
+ sanitize — strip HTML/script tags
1048
+ validate — throw if field value fails a rule
1049
+ transform — apply a custom mapping function
1050
+ uuid — generate a UUID for the field
1051
+ slug — generate a URL slug from another field
1052
+ timestamp — set field to current timestamp (ISO 8601)
1053
+ round — round number to N decimal places
1054
+ clamp — clamp number to a min/max range
1055
+ truncate — truncate string to max length
1056
+ default — set a default value if null/undefined
1057
+ normalizePhone — normalise phone number format
1058
+ normalizeEmail — lowercase + trim email
1059
+ parseJson — parse JSON string to object
1060
+ stringifyJson — serialise object to JSON string
1061
+ compute — derive field value from other fields (runs before validate)
1062
+
1063
+ Execution order is fixed regardless of declaration order:
1064
+ default → compute → validate → trim → lowercase/uppercase/normalize* →
1065
+ sanitize → truncate → slug → round/clamp → parseJson/stringifyJson → uuid/timestamp → transform
1066
+
1067
+ Example:
1068
+ await ductape.database.insert({
1069
+ product, env, database: 'users', entity: 'users',
1070
+ data: { email: ' User@Example.com ', password: 'secret', username: 'My App User' },
1071
+ preSave: [
1072
+ { field: 'email', operation: 'normalizeEmail' },
1073
+ { field: 'password', operation: 'hash' },
1074
+ { field: 'username', operation: 'slug', target: 'slug' },
1075
+ ],
1076
+ });
1077
+ `.trim(),
1078
+
1079
+ triggers: `
1080
+ DUCTAPE DATABASE TRIGGERS
1081
+
1082
+ Triggers fire automatically in response to database write events.
1083
+ Defined via the Workbench UI or databases.trigger.create (admin SDK).
1084
+
1085
+ Events (TriggerEvent):
1086
+ beforeInsert / afterInsert
1087
+ beforeUpdate / afterUpdate
1088
+ beforeDelete / afterDelete
1089
+ beforeWrite / afterWrite (any write)
1090
+
1091
+ Timing (TriggerTiming):
1092
+ sync — block the write until trigger completes (adds latency)
1093
+ async — fire and forget (default for notifications, cache, broker)
1094
+ queued — enqueue for background processing
1095
+
1096
+ Action types (TriggerActionType):
1097
+ database.* — insert/update/delete/query another collection
1098
+ storage.* — upload/delete/copy a file
1099
+ notification.* — email/SMS/push/callback
1100
+ broker.publish — publish to a message broker topic
1101
+ cache.* — set/invalidate/delete a cache key
1102
+ feature.* — execute or dispatch a Ductape feature
1103
+ action.execute — call an app action
1104
+ agent.run — run an AI agent
1105
+ quota.run / fallback.run / healthcheck.run
1106
+ vector.upsert / vector.delete
1107
+ session.revoke
1108
+ log.create
1109
+ custom.function / custom.http
1110
+
1111
+ All triggers support an optional condition (field comparisons) to gate execution.
1112
+ All triggers support retry config: { maxAttempts, delay, backoff: "fixed"|"exponential" }.
1113
+
1114
+ Triggers are admin-only configuration — they cannot be set at runtime via ductape_execute.
1115
+ Use ductape_cli with the Workbench or databases.trigger.create via the admin SDK.
1116
+ `.trim(),
1117
+
1118
+ aggregations: `
1119
+ DUCTAPE DATABASE AGGREGATIONS
1120
+
1121
+ databases.aggregate [{ product, env, database, entity, aggregations, where?, groupBy? }]
1122
+
1123
+ aggregations array — each entry:
1124
+ { type: "count"|"sum"|"avg"|"min"|"max", field?: string, alias: string }
1125
+
1126
+ Example — total revenue grouped by category:
1127
+ ductape_execute("databases.aggregate", [{
1128
+ product: "my-product",
1129
+ env: "prd",
1130
+ database: "sales-db",
1131
+ entity: "orders",
1132
+ aggregations: [
1133
+ { type: "sum", field: "amount", alias: "totalRevenue" },
1134
+ { type: "count", alias: "orderCount" },
1135
+ { type: "avg", field: "amount", alias: "avgOrderValue" },
1136
+ ],
1137
+ groupBy: ["category"],
1138
+ where: { status: { $eq: "completed" } },
1139
+ }])
1140
+
1141
+ Supported where operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $like, $ilike
1142
+ groupBy: array of field names to group by (omit for global aggregation)
1143
+ `.trim(),
1144
+
1145
+ migrations: `
1146
+ DUCTAPE DATABASE MIGRATIONS
1147
+
1148
+ Migrations are versioned SQL/NoSQL schema change scripts managed per database component.
1149
+
1150
+ Create a migration:
1151
+ ductape_execute("databases.migration.create", [{
1152
+ product: "my-product",
1153
+ database: "core-db",
1154
+ data: {
1155
+ name: "add users table",
1156
+ tag: "001-add-users",
1157
+ value: {
1158
+ up: ["CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT NOT NULL UNIQUE)"],
1159
+ down: ["DROP TABLE users"],
1160
+ },
1161
+ },
1162
+ }])
1163
+
1164
+ Run migrations (applies all pending):
1165
+ ductape_execute("databases.migration.run", [migrations, { env: "prd" }])
1166
+
1167
+ Rollback:
1168
+ ductape_execute("databases.migration.rollback", [migrations, 1]) // roll back 1
1169
+
1170
+ Check status:
1171
+ ductape_execute("databases.migration.status", [migrations])
1172
+
1173
+ History:
1174
+ ductape_execute("databases.migration.history", [])
1175
+
1176
+ Via CLI:
1177
+ ductape_cli("db migrate") // run pending
1178
+ ductape_cli("db migrate rollback") // roll back last
1179
+ ductape_cli("db migrate rollback -n 3")
1180
+
1181
+ MongoDB: migrations run as raw Mongo shell commands in the up/down arrays.
1182
+ `.trim(),
1183
+
1184
+ indexes: `
1185
+ DUCTAPE DATABASE INDEXES
1186
+
1187
+ Create an index:
1188
+ ductape_execute("databases.schema.createIndex", [
1189
+ "collection_name",
1190
+ ["field1", "field2"], // simple array of field names
1191
+ { unique: true, name: "idx_email_unique" } // options (optional)
1192
+ ])
1193
+
1194
+ // Ordered index:
1195
+ ductape_execute("databases.schema.createIndex", [
1196
+ "orders",
1197
+ [{ field: "createdAt", order: "DESC" }, { field: "userId", order: "ASC" }],
1198
+ { name: "idx_orders_date_user" }
1199
+ ])
1200
+
1201
+ Drop an index:
1202
+ ductape_execute("databases.schema.dropIndex", ["collection_name", "index_name"])
1203
+
1204
+ List indexes on a collection:
1205
+ ductape_execute("databases.schema.indexes", ["collection_name"])
1206
+
1207
+ Performance guidance:
1208
+ - Index fields used in WHERE clauses, JOIN conditions, and ORDER BY.
1209
+ - Compound indexes: put high-cardinality fields first.
1210
+ - Unique indexes enforce uniqueness at the database level.
1211
+ - MongoDB: compound indexes support prefix queries; order matters.
1212
+ - PostgreSQL/MySQL: B-tree indexes by default; use partial indexes for sparse columns.
1213
+ - Avoid over-indexing writes-heavy collections — each index slows inserts/updates.
1214
+
1215
+ Before adding an index ask the user which fields they query/filter on and expected data volume.
1216
+ `.trim(),
1217
+
1218
+ performance: `
1219
+ DUCTAPE DATABASE PERFORMANCE GUIDANCE
1220
+
1221
+ 1. Queries
1222
+ - Always pass select: ["field1", "field2"] to avoid fetching unused columns.
1223
+ - Use limit + offset for pagination; add an index on the ORDER BY field.
1224
+ - Use count/sum/avg/aggregate instead of fetching all rows and computing in code.
1225
+
1226
+ 2. Indexes (see ductape_docs({ topic: "indexes" }) for syntax)
1227
+ - Index every WHERE / ORDER BY field.
1228
+ - Compound index order: equality fields first, range fields last.
1229
+ - For MongoDB: covered queries (index holds all projected fields) skip the document read.
1230
+
1231
+ 3. Connections
1232
+ - The SDK pools connections per product+env+database triple.
1233
+ - Call databases.connect once per process; the pool reuses the connection.
1234
+ - databases.disconnect releases the pool — call only on shutdown.
1235
+
1236
+ 4. Transactions (see ductape_docs({ topic: "transactions" }))
1237
+ - Keep transactions short; long-lived transactions block row locks (PostgreSQL/MySQL).
1238
+ - For MongoDB on M10+, use sessions for multi-document writes — not for reads.
1239
+
1240
+ 5. Caching
1241
+ - For read-heavy, rarely-changing data use caches.get before databases.query.
1242
+ - Invalidate cache keys in an afterWrite trigger (see ductape_docs({ topic: "triggers" })).
1243
+ `.trim(),
1244
+
1245
+ actions: `
1246
+ DUCTAPE DATABASE ACTIONS
1247
+
1248
+ A database action is a saved query or mutation (SQL string or NoSQL command) stored
1249
+ on the Ductape product and executed by tag at runtime.
1250
+
1251
+ Create an action (admin):
1252
+ ductape_execute("databases.action.create", [{
1253
+ product: "my-product",
1254
+ database: "core-db",
1255
+ data: {
1256
+ tag: "get-active-users",
1257
+ name: "Get active users",
1258
+ description: "Returns all users with status=active",
1259
+ type: "sql", // "sql" or "nosql"
1260
+ query: "SELECT * FROM users WHERE status = :status",
1261
+ },
1262
+ }])
1263
+
1264
+ Dispatch an action at runtime:
1265
+ → CALL ductape_generate_payload FIRST to get the canonical input shape.
1266
+ ductape_execute("databases.action.dispatch", [{
1267
+ product: "my-product",
1268
+ env: "prd",
1269
+ database: "core-db",
1270
+ action: "get-active-users",
1271
+ input: { status: "active" },
1272
+ }])
1273
+
1274
+ List actions for a database:
1275
+ ductape_execute("databases.action.list", ["database_tag"])
1276
+
1277
+ Fetch / update / delete:
1278
+ ductape_execute("databases.action.fetch", ["action_tag"])
1279
+ ductape_execute("databases.action.update", ["my-product", "action_tag", { query: "..." }])
1280
+ ductape_execute("databases.action.delete", ["action_tag"])
1281
+
1282
+ Actions are the preferred way to encapsulate complex or reused queries — they can be
1283
+ scheduled, dispatched with retries, and audited via logs.
1284
+ `.trim(),
1285
+
1286
+ graphs: `
1287
+ DUCTAPE GRAPH DATABASES
1288
+
1289
+ Supported engines: neo4j | neptune | cosmos-gremlin | spanner-graph | arangodb | memgraph
1290
+
1291
+ Registration (admin — ductape_cli):
1292
+ ductape_cli("resources graphs create -f graph.json")
1293
+ File: { name, tag, type, envs: [{ slug, connection_url, username?, password?, graphName?, region? }] }
1294
+ Sensitive fields (connection_url, username, password) are auto-wrapped as $Secret{...} when
1295
+ a productTag is supplied — do NOT pre-wrap them yourself.
1296
+
1297
+ Node operations:
1298
+ graph.createNode [{ labels: string[], properties: { key: value } }, transaction?]
1299
+ graph.findNodes [{ labels?, where?, limit?, skip? }, transaction?]
1300
+ graph.findNodeById [id, transaction?]
1301
+ graph.updateNode [{ id, properties }, transaction?]
1302
+ graph.deleteNode [{ id, detach?: boolean }, transaction?]
1303
+ graph.mergeNode [{ labels, matchProps, setProps? }, transaction?]
1304
+ graph.addLabels / removeLabels / setLabels [{ id, labels }, transaction?]
1305
+
1306
+ Relationship operations:
1307
+ graph.createRelationship [{ fromId, toId, type, properties? }, transaction?]
1308
+ graph.findRelationships [{ type?, where?, limit? }, transaction?]
1309
+ graph.updateRelationship [{ id, properties }, transaction?]
1310
+ graph.deleteRelationship [{ id }, transaction?]
1311
+ graph.mergeRelationship [{ fromId, toId, type, matchProps?, setProps? }, transaction?]
1312
+
1313
+ Traversal:
1314
+ graph.traverse [{ startId, direction: "in"|"out"|"both", relationshipTypes?, maxDepth?, where? }]
1315
+ graph.shortestPath [{ fromId, toId, relationshipType?, maxDepth? }]
1316
+ graph.allPaths [{ fromId, toId, relationshipType?, maxDepth? }]
1317
+ graph.getNeighborhood [{ id, depth?, relationshipTypes? }]
1318
+ graph.findConnectedComponents [{ labels? }]
1319
+
1320
+ Raw query (provider-native Cypher/Gremlin):
1321
+ graph.query [cypher_query: string, params?: { key: value }, transaction?]
1322
+
1323
+ Full-text and vector search (where supported):
1324
+ graph.fullTextSearch [{ index, query, limit? }]
1325
+ graph.vectorSearch [{ index, vector: number[], topK? }]
1326
+
1327
+ Transactions:
1328
+ graph.beginTransaction [options?: { isolationLevel?, timeout? }] → transaction object
1329
+ graph.commitTransaction [transaction]
1330
+ graph.rollbackTransaction [transaction]
1331
+ Or use withTransaction (managed — auto-commit/rollback):
1332
+ graph.withTransaction [graphTag, env, callback, options?]
1333
+ Transaction support varies by engine — ask the user which engine they use before enabling.
1334
+ neo4j: full ACID; neptune: limited; arangodb: multi-document; memgraph: full ACID.
1335
+
1336
+ Schema management:
1337
+ graph.createNodeIndex [{ label, field, type: "btree"|"fulltext"|"vector" }]
1338
+ graph.createNodeConstraint [{ label, field, type: "unique"|"exists" }]
1339
+ graph.dropIndex / dropConstraint / listIndexes / listConstraints
1340
+
1341
+ Saved actions (parameterized queries stored on the product):
1342
+ graph.createAction [{ graphTag, tag, name, query, params? }, productTag?]
1343
+ graph.listActions [graphTag?, productTag?]
1344
+ graph.dispatch [data] ← call ductape_generate_payload FIRST
1345
+
1346
+ Supported index types: btree | fulltext | vector | range | point | text
1347
+ Supported constraint types: UNIQUE | EXISTS | NODE_KEY
1348
+ `.trim(),
1349
+
1350
+ storage: `
1351
+ DUCTAPE FILE STORAGE
1352
+
1353
+ Supported providers: aws (S3) | gcp (GCS) | azure (Blob Storage)
1354
+
1355
+ Registration (admin — ductape_cli):
1356
+ ductape_cli("resources storage create -f storage.json")
1357
+ File: { name, tag, envs: [{ slug, type: "aws"|"gcp"|"azure", config: { ... } }] }
1358
+ For cloud-linked envs: set config.cloud to the connection tag; omit raw credentials.
1359
+ For all envs in the product must be covered — see ductape_docs({ topic: "cloud" }) for
1360
+ import-persist-all workflow when connecting existing buckets.
1361
+
1362
+ Operations (runtime):
1363
+ storage.upload [{ product, env, storage, fileName, buffer: string|Buffer, mimeType? }]
1364
+ storage.download [{ product, env, storage, fileName }] → { content, size, mimeType }
1365
+ storage.remove [{ product, env, storage, fileName }]
1366
+ storage.listFiles [{ product, env, storage, prefix?, limit?, continuationToken? }]
1367
+ storage.getSignedUrl [{ product, env, storage, fileName, expiresIn?: number, action?: "read"|"write" }]
1368
+ storage.stats [{ product, env, storage, prefix?, cache? }] → file counts by type
1369
+ storage.testConnection [{ product, env, storage }]
1370
+
1371
+ Background dispatch (fire-and-forget):
1372
+ storage.dispatch [{ product, env, storage, operation, input, schedule? }]
1373
+ → CALL ductape_generate_payload FIRST (operation_family="storage", method="dispatch")
1374
+ → Returns { jobId, status: "queued" } immediately; actual operation runs in background.
1375
+ → schedule: { start_at?, cron?, every?, limit?, endDate?, tz? }
1376
+
1377
+ Caching:
1378
+ download, listFiles, getSignedUrl, and stats all support a cache option.
1379
+ Pass cache: "cache_tag" to check the product cache before hitting the provider.
1380
+ Cached results are stored fire-and-forget on miss.
1381
+
1382
+ Notes:
1383
+ - buffer can be a Node.js Buffer (server) or a base64 string (JSON proxy clients).
1384
+ - Cloud-linked configs inject runtime credentials via the cloud connection; no plaintext keys stored.
1385
+ - $Secret{} references in config are resolved at runtime if cloud-link resolution fails.
1386
+ `.trim(),
1387
+
1388
+ cloud: `
1389
+ DUCTAPE CLOUD CONNECTIONS
1390
+
1391
+ Cloud connections link your workspace to an external cloud provider so Ductape can manage
1392
+ resources (databases, storage, brokers, graphs, vectors) on your behalf.
1393
+
1394
+ Supported providers: aws | gcp | azure | mongodb_atlas | neo4j_aura
1395
+ Auth modes: iam_role | oauth | service_principal | workload_identity | api_key
1396
+
1397
+ Create a connection (admin — ductape_cli):
1398
+ ductape_cli("cloud connections create --provider aws --name my-aws-conn")
1399
+ Returns: external_id, trust_policy, setup_instructions, setup_url
1400
+ AWS: the caller must create an IAM role using the returned trust policy, then complete:
1401
+ ductape_cli("cloud connections complete <tag> --role-arn arn:aws:iam::...")
1402
+ GCP: complete with project_id, service_account_email, service_account_json
1403
+ Azure: complete with tenant_id, subscription_id, client_id, client_secret, default_location
1404
+ Atlas: complete with atlas_public_key, atlas_private_key
1405
+ Aura (Neo4j): complete with aura_client_id, aura_client_secret, aura_instance_id
1406
+
1407
+ Validate a connection:
1408
+ ductape_cli("cloud connections validate <tag>")
1409
+ → { valid, status, message?, tested_at? }
1410
+
1411
+ List / fetch / delete:
1412
+ ductape_cli("cloud connections list")
1413
+ ductape_cli("cloud connections fetch <tag>")
1414
+
1415
+ Discover resources on a connection:
1416
+ ductape_cli("cloud resources list -f query.json --json")
1417
+ File: { cloud: "<tag>", service: "gcs"|"s3"|"blob"|"rds"|"atlas-cluster"|..., region? }
1418
+ → returns list of available resources (buckets, clusters, instances, etc.)
1419
+
1420
+ Import an existing resource and register it on the product:
1421
+ Use import-persist-all for multi-env products (required):
1422
+ ductape_cli("cloud resources import-persist-all -f all-envs.json --json")
1423
+ File is a JSON ARRAY — one entry per env, same product + component tag across all entries.
1424
+ Each entry: { cloud, service, type, product, component, env, resource, region?, dbName? }
1425
+ Supported service identifiers: s3, gcs, blob, rds, postgresql, cloudsql, sqs, pubsub,
1426
+ servicebus, neptune, cosmos-gremlin, opensearch, azure-search, atlas-cluster, aura-instance
1427
+
1428
+ Provision a brand-new resource and register it:
1429
+ ductape_cli("cloud resources provision-persist-all -f all-envs.json --json")
1430
+ Additional per-entry fields: tier, region/location, waitForReady.
1431
+ List available tiers first: ductape_cli("cloud tiers --provider aws --type database --db-type postgresql --json")
1432
+ NEVER infer region, tier, or cost — always list and confirm with the user first.
1433
+ Atlas and Neo4j Aura are IMPORT-ONLY — provision is not supported for these providers.
1434
+
1435
+ VPC connector (private networking):
1436
+ ductape_cli("cloud connections vpc update <tag> --vpc-id vpc-xxx --subnet-ids subnet-1,subnet-2")
1437
+ ductape_cli("cloud connections vpc status <tag>")
1438
+ VPC connector enables Ductape to reach resources in a private VPC; agent_status:
1439
+ pending → connected → disconnected
1440
+
1441
+ Scopes a connection can cover: storage | broker | database | graph | vector | cache
1442
+
1443
+ Component types accepted by import/provision:
1444
+ storage | messageBrokers | databases | graphs | vectors | caches
1445
+ `.trim(),
1446
+
1447
+ vector: `
1448
+ DUCTAPE VECTOR DATABASES
1449
+
1450
+ Supported adapters: pinecone | qdrant | weaviate | opensearch | azure-search | vertex-vector-search
1451
+ (chroma, milvus, pgvector are declared but not yet implemented — do not use them)
1452
+
1453
+ Registration (admin — ductape_cli):
1454
+ ductape_cli("resources vectors create -f vector.json")
1455
+ File: { name, tag, type, dimensions: number, metric?: "cosine"|"euclidean"|"dotproduct",
1456
+ envs: [{ slug, endpoint?, apiKey?, region?, index?, namespace? }] }
1457
+
1458
+ Runtime operations:
1459
+ vector.upsert [{ product, env, tag, vectors: [{id, values: number[], metadata?}], namespace? }]
1460
+ vector.upsertOne [{ product, env, tag, id, values: number[], metadata?, namespace? }]
1461
+ vector.query [{ product, env, tag, vector: number[], topK?, filter?, namespace?,
1462
+ includeValues?, includeMetadata? }]
1463
+ vector.findSimilar [{ product, env, vector, values: number[], topK?, filter?, namespace? }]
1464
+ vector.fetchOne [{ product, env, vector, id, namespace? }]
1465
+ vector.fetchVectors [{ product, env, vector, ids: string[], namespace? }]
1466
+ vector.updateVector [{ product, env, vector, id, values?, setMetadata?, mergeMetadata?, namespace? }]
1467
+ vector.updateMetadata [{ product, env, vector, id, metadata: { key: value }, merge?, namespace? }]
1468
+ merge: true → deep-merges metadata rather than replacing it
1469
+ vector.deleteByIds [{ product, env, vector, ids: string[], namespace? }]
1470
+ vector.deleteAll [{ product, env, vector, namespace? }]
1471
+ vector.count [{ product, env, vector, namespace? }]
1472
+
1473
+ Namespace management:
1474
+ vector.listNamespaces [{ product, env, vector }]
1475
+ vector.deleteNamespace [{ product, env, vector, namespace }]
1476
+
1477
+ Listing (paginated):
1478
+ vector.listVectors [{ product, env, vector, namespace?, prefix?, limit?, cursor? }]
1479
+ vector.listAllVectors [{ product, env, vector, namespace?, prefix? }]
1480
+ listAllVectors auto-paginates until cursor exhausted — avoid on large indexes.
1481
+
1482
+ Index management:
1483
+ vector.describeIndex [{ product, env, vector }]
1484
+ vector.getStats [{ product, env, vector }] → totalVectorCount per namespace
1485
+ vector.createIndex [{ product, env, vector, name, dimensions, metric?, replicas?, shards? }]
1486
+ vector.deleteIndex [{ product, env, vector, name }]
1487
+ vector.listIndexes [{ product, env, vector }]
1488
+
1489
+ Distance metrics: cosine | euclidean | dotproduct | manhattan | hamming
1490
+ Index types: flat | ivf | hnsw | pq | ivf_pq | annoy
1491
+ Feature flags per adapter: metadata_filtering | namespaces | hybrid_search | batch_operations |
1492
+ index_management | vector_updates | sparse_vectors | multi_vector | aggregations
1493
+
1494
+ Call vector.supportsFeature(feature) or vector.getSupportedFeatures() to check what the
1495
+ chosen adapter supports before using a feature.
1496
+ `.trim(),
1497
+
1498
+ warehouse: `
1499
+ DUCTAPE WAREHOUSE
1500
+
1501
+ The Warehouse is a unified query layer over the three structured data stores: Database, Graph, and Vector.
1502
+ It provides a single API for cross-store operations without having to address each service separately.
1503
+
1504
+ Warehouse is available on the SDK instance as ductape.warehouse (TypeScript) or Warehouse (C#/Go/Java).
1505
+ It is NOT a separate Ductape resource — it wraps the existing database, graph, and vector components
1506
+ that are already registered on the product.
1507
+
1508
+ Key use cases:
1509
+ - Run a relational query, a graph traversal, and a vector similarity search in a single call.
1510
+ - Build recommendation pipelines: graph neighbors → vector re-rank → database hydration.
1511
+ - Federated search across multiple store types with a single await.
1512
+
1513
+ Usage (TypeScript SDK):
1514
+ const result = await ductape.warehouse.query({
1515
+ product: "my-product",
1516
+ env: "prd",
1517
+ database: { tag: "core-db", entity: "products", where: { active: { $eq: true } }, select: ["id", "name"] },
1518
+ graph: { tag: "reco-graph", startId: userId, direction: "out", maxDepth: 2 },
1519
+ vector: { tag: "embedding-store", vector: queryEmbedding, topK: 10 },
1520
+ });
1521
+
1522
+ result.database → relational rows
1523
+ result.graph → traversal nodes/relationships
1524
+ result.vector → similarity hits with scores
1525
+
1526
+ Warehouse context is set at SDK init time (env + workspaceId) and shared across all three stores.
1527
+ Underlying store calls use the same auth and product context; individual store errors are surfaced per store.
1528
+
1529
+ Before using Warehouse:
1530
+ 1. Ensure the database, graph, and vector components are registered on the product.
1531
+ 2. Confirm the env slug exists on all three stores.
1532
+ 3. Each store can be queried independently — pass only the fields for the stores you need.
1533
+
1534
+ Warehouse does not support writes — use the individual service APIs (database.insert, graph.createNode,
1535
+ vector.upsert) for mutations.
1536
+ `.trim(),
1537
+
1538
+ secrets: `
1539
+ DUCTAPE SECRETS
1540
+
1541
+ Secrets are workspace-level encrypted key-value pairs. They are referenced in resource configs,
1542
+ connection URLs, and any string field using the $Secret{KEY_NAME} syntax.
1543
+
1544
+ Create (admin — ductape_cli or ductape_execute):
1545
+ ductape_execute("secrets.create", [{
1546
+ key: "STRIPE_API_KEY",
1547
+ value: "sk_live_...", // plaintext — encrypted AES-256-GCM client-side before send
1548
+ description: "Stripe live key",
1549
+ token_type: "api", // "api" | "password" | "certificate"
1550
+ scope: ["my-product"], // which products can read this secret
1551
+ envs: ["prd"], // which env slugs can read this secret
1552
+ expires_at: 1800000000, // optional epoch ms
1553
+ }])
1554
+ The server never receives the plaintext value. Encryption uses the workspace private key.
1555
+
1556
+ Fetch / resolve:
1557
+ ductape_execute("secrets.fetch", ["STRIPE_API_KEY"]) → decrypted string value
1558
+ ductape_execute("secrets.exists", ["STRIPE_API_KEY"]) → boolean
1559
+ ductape_execute("secrets.validate", ["$Secret{STRIPE_API_KEY}"]) → { valid, missingKeys, existingKeys }
1560
+ ductape_execute("secrets.resolve", ["$Secret{STRIPE_API_KEY}", { env: "prd" }]) → resolved string
1561
+
1562
+ $Secret{} reference syntax:
1563
+ - Embed anywhere a string is accepted: "postgresql://$Secret{DB_USER}:$Secret{DB_PASS}@host/db"
1564
+ - Resolved at runtime before the value is used by the consuming service (storage, broker, graph, etc.)
1565
+ - The in-memory cache stores the encrypted form only; decryption happens on each cache hit.
1566
+ - Cache TTL: 5 minutes. Clear with: secrets.clearCache()
1567
+
1568
+ Lifecycle:
1569
+ ductape_execute("secrets.revoke", ["KEY"]) → disables without deleting (recoverable)
1570
+ ductape_execute("secrets.delete", ["KEY"]) → permanent deletion (irreversible)
1571
+ ductape_execute("secrets.update", ["KEY", { value: "new_value", expires_at: ... }])
1572
+
1573
+ List all secrets (keys only — values are not returned in list):
1574
+ ductape_execute("secrets.list", [])
1575
+
1576
+ Important:
1577
+ - Secrets are workspace-scoped, not product-scoped. scope[] and envs[] control access.
1578
+ - Other services (storage, broker, graph, etc.) resolve $Secret{} references automatically
1579
+ using the singleton secrets service — no manual resolution needed in most cases.
1580
+ - Never log or return resolved secret values to end users.
1581
+ `.trim(),
1582
+
1583
+ apps: `
1584
+ DUCTAPE APPS
1585
+
1586
+ An app is a versioned API integration definition. It contains environments (base URLs), actions
1587
+ (individual endpoint specs), auth schemes, webhooks, variables, and constants.
1588
+
1589
+ Create an app (admin — ductape_cli):
1590
+ ductape_cli("apps create --name \\"Email Service\\" --description \\"Transactional email\\"")
1591
+ ductape_cli("app.init", ["app_tag"]) → loads app into builder state
1592
+
1593
+ Import from a file:
1594
+ ductape_cli("apps import <file.json> -t postman|openapi")
1595
+ Supports Postman v2.1 collection and OpenAPI 3.0 spec.
1596
+
1597
+ Manage environments (base URLs per stage):
1598
+ ductape_execute("app.environments.create", [app_tag, { slug, env_name, base_url }])
1599
+ ductape_execute("app.environments.list", [app_tag])
1600
+
1601
+ Manage actions (individual API endpoints):
1602
+ ductape_execute("actions.create", [app_tag, {
1603
+ tag, name, resource: "/users/{id}", method: "GET"|"POST"|"PUT"|"PATCH"|"DELETE",
1604
+ body?: { fieldName: { type, required? } },
1605
+ params?: { id: { type: "string" } },
1606
+ query?: { filter: { type: "string" } },
1607
+ headers?: { Authorization: { type: "string" } },
1608
+ response?: { status_code: 200, success: true, body: { ... }, response_format: "json" },
1609
+ }])
1610
+ ductape_execute("actions.update", [app_tag, action_tag, data])
1611
+ ductape_execute("actions.list", [app_tag])
1612
+ ductape_execute("actions.fetch", [app_tag, action_tag])
1613
+
1614
+ Run an action at runtime:
1615
+ → CALL ductape_generate_payload FIRST (operation_family="action", method="run", targets={app, action})
1616
+ ductape_execute("actions.run", [{ product, env, app, action, input: { "body:field": value } }])
1617
+ ductape_execute("actions.dispatch", [{ product, env, app, action, input, schedule? }])
1618
+
1619
+ Auth schemes (how the app authenticates outbound requests):
1620
+ Setup types: header | bearer | basic | oauth2 | apikey
1621
+ ductape_execute("auths.create", [app_tag, { tag, name, setup_type, expiry, period, action_tag? }])
1622
+ ductape_execute("auths.list", [app_tag])
1623
+
1624
+ Webhooks (inbound events from the external service):
1625
+ ductape_execute("webhooks.create", [app_tag, { tag, name, description, envs: [{ slug, registration_url?, method? }] }])
1626
+ ductape_execute("webhooks.events.create", [app_tag, { tag, name, selector, description, sample }])
1627
+
1628
+ Variables (per-env mutable values) and Constants (fixed values):
1629
+ ductape_execute("app.variables.create", [app_tag, { key, value, env_slug }])
1630
+ ductape_execute("app.constants.create", [app_tag, { key, value }])
1631
+
1632
+ Connecting an app to a product (after creation):
1633
+ ductape_execute("product.apps.add", [product_tag, {
1634
+ access_tag: "app_access_tag",
1635
+ envs: [{ app_env_slug: "production", product_env_slug: "prd",
1636
+ variables: [{ key: "BASE_URL", value: "https://api.example.com" }],
1637
+ auth: { auth_tag: "api-key-auth", data: "$Secret{API_KEY}" } }]
1638
+ }])
1639
+ ductape_execute("product.apps.list", [product_tag])
1640
+ ductape_execute("product.apps.fetch", [product_tag, access_tag])
1641
+ `.trim(),
1642
+
1643
+ products: `
1644
+ DUCTAPE PRODUCTS
1645
+
1646
+ A product is the top-level namespace for all Ductape infrastructure: apps, databases, graphs,
1647
+ vectors, storage, brokers, sessions, caches, notifications, resilience, features, jobs, and envs.
1648
+ Every SDK service call resolves within a product context.
1649
+
1650
+ Create a product:
1651
+ ductape_cli("products create --name \\"My App\\" --tag my-app")
1652
+ ductape_execute("product.create", [{ name: "My App", tag: "my-app",
1653
+ envs: [{ slug: "dev", name: "Development" }, { slug: "prd", name: "Production" }] }])
1654
+
1655
+ Environments — every resource's envs array MUST cover all product env slugs:
1656
+ ductape_execute("product.environments.create", [product_tag, { slug, env_name, description, active? }])
1657
+ ductape_execute("product.environments.list", [product_tag])
1658
+ ductape_execute("product.environments.fetch", [product_tag, slug])
1659
+ BEFORE registering any resource, always run environments.list and collect all slugs.
1660
+
1661
+ Fetch / update:
1662
+ ductape_execute("product.fetch", [product_tag])
1663
+ ductape_execute("product.update", [product_tag, { name?, description? }])
1664
+
1665
+ Connect apps to a product:
1666
+ See ductape_docs({ topic: "apps" }) for product.apps.add / product.apps.list.
1667
+
1668
+ Resource registration (all via ductape_cli or ductape_execute — see per-topic docs):
1669
+ databases → ductape_docs({ topic: "transactions" })
1670
+ storage → ductape_docs({ topic: "storage" })
1671
+ graphs → ductape_docs({ topic: "graphs" })
1672
+ vectors → ductape_docs({ topic: "vector" })
1673
+ events → ductape_docs({ topic: "events" })
1674
+ caches → ductape_docs({ topic: "caches" })
1675
+ notifications → ductape_docs({ topic: "notifications" })
1676
+ sessions → ductape_docs({ topic: "sessions" })
1677
+ resilience → ductape_docs({ topic: "resilience" })
1678
+ features → ductape_docs({ topic: "features" })
1679
+
1680
+ Product structure (IProduct fields):
1681
+ _id, workspace_id, name, tag, description, private_key,
1682
+ apps[], envs[], databases[], graphs[], vectors[], storage[], messageBrokers[],
1683
+ caches[], sessions[], notifications[], quota[], fallback[], healthchecks[],
1684
+ workflows[] (features), models[], agents[], jobs[]
1685
+
1686
+ Bootstrap (single API call returning product context + component config + private key):
1687
+ Each service makes a single bootstrap call at first use; results are cached in BootstrapCache
1688
+ (Redis when available). This avoids repeated round-trips in high-frequency paths.
1689
+ `.trim(),
1690
+
1691
+ sessions: `
1692
+ DUCTAPE SESSIONS
1693
+
1694
+ A session is a named JWT schema on a product. It defines:
1695
+ - tag / name — unique identifier and display name
1696
+ - expiry + period — how long each issued JWT is valid (duration, not an absolute date)
1697
+ - selector — the dot-path of the field inside the data object that is the PRIMARY
1698
+ user identifier (e.g. "playerId"). This field becomes the lookup key
1699
+ for revoke, list, and analytics. It MUST exist in the schema.
1700
+ - schema — the complete shape of data that will be embedded in the JWT payload.
1701
+ Each field is declared as { type, required?, description? }.
1702
+ This is a TYPE DECLARATION, not sample data. The actual values
1703
+ are passed at runtime when calling sessions.start.
1704
+
1705
+ IMPORTANT — schema field format: { fieldName: { type, required?, description? } }
1706
+ type values: "string" | "number" | "boolean" | "object" | "array"
1707
+ Do NOT use the shorthand { fieldName: "string" } — pass the full object form.
1708
+
1709
+ Example (game product — player identity in JWT):
1710
+ ductape_execute("sessions.create", [product_tag, {
1711
+ tag: "player-session",
1712
+ name: "Player Session",
1713
+ expiry: 24,
1714
+ period: "hours",
1715
+ selector: "playerId", // primary identifier — must be in schema below
1716
+ schema: {
1717
+ playerId: { type: "string", required: true, description: "Unique player ID" },
1718
+ username: { type: "string", required: true, description: "Player display name" },
1719
+ role: { type: "string", required: false, description: "admin | player | guest" },
1720
+ accountId: { type: "string", required: false, description: "Linked account ID" },
1721
+ },
1722
+ }])
1723
+
1724
+ Runtime — create a session (sign a JWT):
1725
+ → CALL ductape_generate_payload FIRST (operation_family="session", method="start", targets={tag})
1726
+ to discover the exact data field names accepted for this session tag.
1727
+ ductape_execute("sessions.start", [{ product, env, tag: "player-session",
1728
+ data: {
1729
+ playerId: "player_abc123", // must match selector — used as the revocation/lookup key
1730
+ username: "ShadowBlade",
1731
+ role: "player",
1732
+ accountId: "acct_xyz",
1733
+ } }])
1734
+ → returns token: "player-session:eyJ..." ← format is always "session_tag:jwt"
1735
+ The token embeds all schema fields in the JWT payload, signed with the product private key.
1736
+
1737
+ Verify a token:
1738
+ ductape_execute("sessions.verify", [{ product, env, tag: "user-session", token: "user-session:eyJ..." }])
1739
+ → decodes JWT + checks revocation blacklist; throws if revoked or expired
1740
+
1741
+ Refresh (atomic token rotation):
1742
+ ductape_execute("sessions.refresh", [{ product, env, tag: "user-session", refreshToken: "..." }])
1743
+ Old refresh token is invalidated server-side atomically — no partial state possible.
1744
+ The refresh token is AES-encrypted JSON (NOT a JWT).
1745
+
1746
+ Revoke:
1747
+ ductape_execute("sessions.revoke", [{ product, env, tag, sessionId?, identifier? }])
1748
+ Requires at least one of sessionId or identifier.
1749
+ Revocation is enforced via a ProcessorAPI blacklist check on every verify() call.
1750
+
1751
+ Analytics:
1752
+ sessions.listActive [{ product, env, tag, identifier?, page?, limit? }]
1753
+ sessions.fetchUsers [{ product, session, env?, page?, limit? }]
1754
+ sessions.fetchUserDetails [{ product, session, identifier, env? }]
1755
+ sessions.fetchDashboard [{ product, session, env? }]
1756
+ → { DAU, WAU, MAU, activityTimeline, peakHours, environmentBreakdown, avgSessionDuration }
1757
+
1758
+ Token format: "session_tag:jwt_token" — pass this format verbatim wherever session is expected.
1759
+ JWT is signed with the product private key (not a symmetric shared secret).
1760
+ `.trim(),
1761
+
1762
+ caches: `
1763
+ DUCTAPE CACHES
1764
+
1765
+ Caches are product-level Redis (or in-memory) stores for temporary key-value data with optional TTL.
1766
+
1767
+ Registration (admin — ductape_cli):
1768
+ ductape_cli("resources caches create -f cache.json")
1769
+ File: { name, tag, type: "redis"|"memcached"|"in-memory",
1770
+ envs: [{ slug, connection_url }] }
1771
+
1772
+ Operations:
1773
+ caches.set [{ product, cache, key, value: string, expiry?: string (ISO 8601), env }]
1774
+ expiry is an ABSOLUTE TIMESTAMP (not a duration).
1775
+ To expire in 1 hour: expiry = new Date(Date.now() + 3600_000).toISOString()
1776
+ To expire in 24 hours: expiry = new Date(Date.now() + 86400_000).toISOString()
1777
+ To never expire: omit expiry entirely.
1778
+ Via MCP: pass an ISO 8601 string e.g. "2026-07-17T12:00:00.000Z"
1779
+ → Writes to Redis synchronously, then fires remote API write in background (non-blocking).
1780
+ caches.get [{ key: string }]
1781
+ → Checks Redis first; on miss falls through to remote API; on hit from API, populates Redis.
1782
+ → Enforces TTL by comparing the stored expiry timestamp against current time (client-side check).
1783
+ caches.clear [{ key: string }]
1784
+ → Deletes from Redis and from remote API.
1785
+ caches.clearAll [{ product, cache, env? }]
1786
+ → Bulk delete via remote API only. Redis may still have stale keys until naturally evicted.
1787
+ caches.fetchValues [{ product, cache, env?, page?, limit?,
1788
+ expiryFilter?: "all"|"expiring"|"permanent"|"expired" }]
1789
+ caches.fetchDashboard [{ product, cache, env? }]
1790
+ → { totalValues, activeValues, expiredValues, totalSize }
1791
+
1792
+ Tier architecture (three tiers applied automatically):
1793
+ Tier 1: in-process Map (SDK metadata cache, 5-min TTL — not for user data)
1794
+ Tier 2: Redis hash (hSet/hGetAll) with optional EXPIRE
1795
+ Tier 3: Remote Ductape API
1796
+
1797
+ Practical examples:
1798
+ // Cache a player leaderboard for 5 minutes:
1799
+ { product, cache: "leaderboard-cache", key: "top-100", value: JSON.stringify(rows),
1800
+ expiry: new Date(Date.now() + 300_000).toISOString(), env: "prd" }
1801
+
1802
+ // Cache a session token lookup for 1 hour:
1803
+ { product, cache: "session-cache", key: "player:u_123", value: token,
1804
+ expiry: new Date(Date.now() + 3_600_000).toISOString(), env: "prd" }
1805
+
1806
+ Important:
1807
+ - Redis is optional; without it all reads/writes go through the remote API.
1808
+ - expiry is stored as a Date field, not a Redis TTL — the expiry check happens client-side on read.
1809
+ - clearAll only clears via the remote API bulk endpoint; Redis may retain stale entries until evicted.
1810
+ - Cache entries are stored as Redis hashes (not plain strings).
1811
+ - Other services (storage, graph, notifications, sessions) also use CacheManager internally —
1812
+ configure a shared Redis URL at SDK init to share the pool.
1813
+ `.trim(),
1814
+
1815
+ notifications: `
1816
+ DUCTAPE NOTIFICATIONS
1817
+
1818
+ Notifications send messages across multiple channels: email, SMS, push, or HTTP callback.
1819
+
1820
+ Create a notification (admin — ductape_execute):
1821
+ ductape_execute("notifications.create", [product_tag, {
1822
+ tag: "welcome-email",
1823
+ name: "Welcome Email",
1824
+ type: "email", // optional hint; actual channels configured per env
1825
+ }])
1826
+
1827
+ Create a message template:
1828
+ ductape_execute("notifications.messages.create", [product_tag, {
1829
+ tag: "welcome-email:default", // format: "notification_tag:message_tag"
1830
+ notification: "welcome-email",
1831
+ subject: { template: "Welcome, {{name}}!", data: { name: "" } },
1832
+ body: { template: "Hi {{name}}, thanks for signing up.", data: { name: "" } },
1833
+ }])
1834
+
1835
+ Send at runtime (one channel at a time):
1836
+ → CALL ductape_generate_payload FIRST (operation_family="notification", method="email.send")
1837
+ notifications.email.send [{ product, env, notification, input: { recipients, subject?, template? } }]
1838
+ notifications.push.send [{ product, env, notification, input: { device_tokens, title?, body?, data? } }]
1839
+ notifications.sms.send [{ product, env, notification, input: { recipients, body? } }]
1840
+ notifications.callback.send [{ product, env, notification, input: { query?, headers?, params?, body? } }]
1841
+
1842
+ Multi-channel send (all channels in parallel):
1843
+ notifications.send [{ product, env, event: "notif_tag:message_tag", input: { ... } }]
1844
+ → success is true if at least one channel succeeds; channel failures do not abort siblings.
1845
+
1846
+ Background dispatch with scheduling:
1847
+ notifications.dispatch [{ product, env, notification, event, input,
1848
+ schedule?: { start_at?, cron?, every?, limit?, endDate?, tz? } }]
1849
+ → Returns { job_id, status: "queued", scheduled_at, recurring, next_run_at }
1850
+
1851
+ Query delivery logs:
1852
+ notifications.getMessages [{ product_tag?, env?, notification_tag?, status?,
1853
+ type?, start_date?, end_date?, page?, limit? }]
1854
+ Status values: pending | sent | failed | reprocessing
1855
+
1856
+ Supported providers:
1857
+ Email: smtp | mailgun | sendgrid | postmark | brevo
1858
+ SMS: twilio | nexmo | plivo | other
1859
+ Push: firebase | expo
1860
+ Callback: any HTTP endpoint
1861
+
1862
+ Channel configuration (envs per notification) is done in the Workbench UI, not via CLI.
1863
+ Notification tag and message tag are ALWAYS passed together as "notification_tag:message_tag".
1864
+ `.trim(),
1865
+
1866
+ resilience: `
1867
+ DUCTAPE RESILIENCE
1868
+
1869
+ Resilience covers three mechanisms: quotas (rate-limited provider pools), fallbacks (automatic
1870
+ provider switching), and healthchecks (continuous probe monitoring with failure actions).
1871
+
1872
+ QUOTAS — rate-limited multi-provider pools:
1873
+ ductape_execute("quotas.create", [product_tag, {
1874
+ tag: "sms-quota",
1875
+ name: "SMS Provider Pool",
1876
+ input: { to: { type: "string", required: true }, message: { type: "string" } },
1877
+ options: [
1878
+ { provider: "twilio", app: "twilio-app", type: "action", event: "send-sms",
1879
+ quota: 1000, uses: 0, retries: 2,
1880
+ input: { "body:to": "$Input{to}", "body:message": "$Input{message}" },
1881
+ output: {} },
1882
+ { provider: "nexmo", app: "nexmo-app", type: "action", event: "send-sms",
1883
+ quota: 500, uses: 0, retries: 1,
1884
+ input: { "body:to": "$Input{to}", "body:body": "$Input{message}" },
1885
+ output: {} },
1886
+ ],
1887
+ }])
1888
+ Providers are tried in order until quota is not exhausted.
1889
+ quotas.run [{ product, env, tag, input }] ← CALL ductape_generate_payload FIRST
1890
+ quotas.dispatch [{ product, env, tag, input, schedule? }]
1891
+
1892
+ FALLBACKS — automatic provider switching on failure:
1893
+ Same schema as quotas but options are ordered: primary first, then fallback(s).
1894
+ Primary is used first; on failure, the next provider is tried automatically.
1895
+ ductape_execute("fallback.create", [product_tag, { tag, name, input: { ... }, options: [...] }])
1896
+ fallback.run [{ product, env, tag, input }] ← CALL ductape_generate_payload FIRST
1897
+ fallback.dispatch [{ product, env, tag, input, schedule? }]
1898
+
1899
+ HEALTHCHECKS — continuous probe with failure notifications:
1900
+ ductape_execute("health.create", [product_tag, {
1901
+ tag: "payment-health",
1902
+ name: "Payment Service Health",
1903
+ probe: { type: "app", app: "stripe-app", event: "ping" },
1904
+ interval: 30000, // ms between checks
1905
+ retries: 3,
1906
+ envs: [{ slug: "prd", input: {} }],
1907
+ onFailure: {
1908
+ notifications: [{ notification: "ops-alerts", message: "payment-down",
1909
+ channels: { email: { recipients: ["ops@example.com"] } } }],
1910
+ webhooks: [{ url: "https://hooks.example.com/alert", method: "POST" }],
1911
+ },
1912
+ }])
1913
+ health.run [{ product, env, tag }] → triggers an immediate probe
1914
+ health.check [{ product, env, tag }] → same as run
1915
+ health.status [{ product, env, tag }] → current health status
1916
+
1917
+ Probe types: app | database | feature | graph | message_broker | storage
1918
+ Failure actions: notification channels, HTTP webhooks, and/or message broker emit — all can
1919
+ be configured simultaneously on the same healthcheck.
1920
+ Input template references: $Input{field} → maps declared input to the probe's action input.
1921
+ Provider status: available | unavailable
1922
+ `.trim(),
1923
+
1924
+ features: `
1925
+ DUCTAPE FEATURES
1926
+
1927
+ A feature is an orchestrated workflow of durable steps. Steps can call app actions, database
1928
+ operations, graph queries, storage uploads, notifications, broker publishes, child features,
1929
+ quotas, fallbacks, and more. Features support rollback, signals, checkpoints, and sleep.
1930
+
1931
+ Step types: action | database | graph | notification | storage | produce | quota | fallback |
1932
+ vector | child_feature | sleep | wait_for_signal | checkpoint
1933
+
1934
+ Create a feature (admin — ductape_execute):
1935
+ ductape_execute("features.create", [product_tag, {
1936
+ tag: "onboard-user",
1937
+ name: "Onboard User",
1938
+ input: { userId: { type: "string", required: true }, email: { type: "string" } },
1939
+ steps: [
1940
+ {
1941
+ tag: "create-account",
1942
+ type: "database",
1943
+ database: "core-db",
1944
+ event: "insert-user",
1945
+ input: { "body:userId": "$Input{userId}", "body:email": "$Input{email}" },
1946
+ options: { retries: 2, timeout: 5000 },
1947
+ },
1948
+ {
1949
+ tag: "send-welcome",
1950
+ type: "notification",
1951
+ notification: "welcome-email",
1952
+ event: "welcome-email:default",
1953
+ input: { "body:email": "$Input{email}" },
1954
+ dependsOn: ["create-account"],
1955
+ options: { allow_fail: true }, // welcome email failure won't abort the feature
1956
+ },
1957
+ ],
1958
+ }])
1959
+
1960
+ Execute at runtime:
1961
+ → CALL ductape_generate_payload FIRST (operation_family="features", method="execute", targets={tag})
1962
+ features.execute [{ product, env, tag, input: { userId: "u_123", email: "..." },
1963
+ idempotency_key?: string, retries?: number, timeout?: number }]
1964
+
1965
+ Background dispatch with scheduling:
1966
+ features.dispatch [{ product, env, feature, input,
1967
+ schedule?: { start_at?, cron?, every?, limit?, endDate?, tz? } }]
1968
+ → Returns { job_id, status: "queued"|"scheduled", scheduled_at, next_run_at }
1969
+
1970
+ Execution management:
1971
+ features.status [executionId] → { status, current_step, completed_steps, output, error }
1972
+ features.cancel [executionId, reason?] → triggers rollback of running steps
1973
+ features.replay [executionId, options?] → re-run with original input
1974
+ features.restart [executionId] → re-run with new input
1975
+ features.resume [executionId] → continue from a checkpoint
1976
+ features.replayFromStep [executionId, stepTag] → replay from a specific step
1977
+ features.history [executionId] → { events[], checkpoints[], replays[], restarts[] }
1978
+ features.stepDetail [executionId, stepTag] → per-step input/output/error/timing
1979
+
1980
+ Signals and queries (for long-running features):
1981
+ features.signal [{ product, env, feature_id, signal: "payment-confirmed", payload? }]
1982
+ features.query [{ product, env, feature_id, query: "current-status", params? }]
1983
+
1984
+ Step input references:
1985
+ $Input{field} → map from feature's declared input
1986
+ $Step{stepTag}{field} → output field from a prior step
1987
+ $StepOutput{field} → current step's own return value
1988
+ $Concat([...parts], delim) → string interpolation
1989
+
1990
+ Rollback strategies: reverse_all | reverse_critical | compensate | none
1991
+ Feature statuses: pending | running | completed | failed | rolled_back | rolling_back | paused
1992
+
1993
+ Code-first (define API — compiles async handler to JSON step schema):
1994
+ features.define({ tag, name, input, handler: async (ctx) => { ... } })
1995
+ ctx provides: ctx.step(), ctx.action.run(), ctx.database.query/insert(),
1996
+ ctx.graph.execute(), ctx.notification.send(), ctx.storage.upload(),
1997
+ ctx.messaging.produce(), ctx.quota.execute(), ctx.fallback.execute(),
1998
+ ctx.sleep(), ctx.waitForSignal(), ctx.setState/getState(), ctx.feature()
1999
+ `.trim(),
2000
+
2001
+ events: `
2002
+ DUCTAPE EVENTS (MESSAGE BROKERS)
2003
+
2004
+ ARCHITECTURE — always two separate steps:
2005
+ Step 1: Register the BROKER COMPONENT (establishes the connection to the broker service).
2006
+ The broker's envs[] holds connection credentials and host/project info, NOT topics.
2007
+ Step 2: Create TOPIC DEFINITIONS on that broker (unlimited; each is a named subject or queue).
2008
+ Topics are separate from the broker registration and added after.
2009
+ A single broker component can have as many topics as needed.
2010
+
2011
+ ━━━ SUPPORTED BROKER TYPES ━━━
2012
+
2013
+ Cloud-managed (provision OR import via cloud connection):
2014
+ GCP Pub/Sub → service: "pubsub", type in envs: "google_pubsub"
2015
+ AWS SQS → service: "sqs", type in envs: "aws_sqs"
2016
+ Azure SvcBus → service: "servicebus", type in envs: "azure_servicebus"
2017
+
2018
+ Self-hosted (import-only — supply connection URL manually):
2019
+ Kafka → type in envs: "kafka"
2020
+ RabbitMQ → type in envs: "rabbitmq"
2021
+ Redis → type in envs: "redis"
2022
+ NATS → type in envs: "nats"
2023
+
2024
+ ━━━ STEP 1A: REGISTER VIA CLOUD CONNECTION (cloud-managed brokers) ━━━
2025
+
2026
+ Provision (create a NEW resource in the cloud):
2027
+ ductape_cli("cloud resources provision-persist-all -f brokers.json --json")
2028
+ File: JSON ARRAY — one entry per env, same product + component tag across all entries.
2029
+ type field: "messageBrokers" (exact — not "messagebrokers" or "events")
2030
+
2031
+ GCP Pub/Sub — creates a new Pub/Sub topic in GCP, stores credentials in secrets:
2032
+ [{"cloud":"gcp-snd","service":"pubsub","type":"messageBrokers",
2033
+ "product":"my-product","component":"notifications-broker","env":"snd",
2034
+ "topicName":"my-product-notifications-snd"},
2035
+ {"cloud":"gcp-prd","service":"pubsub","type":"messageBrokers",
2036
+ "product":"my-product","component":"notifications-broker","env":"prd",
2037
+ "topicName":"my-product-notifications-prd"}]
2038
+ If topicName is omitted a timestamped name is generated — always supply it explicitly.
2039
+
2040
+ AWS SQS — creates a new SQS queue per env:
2041
+ [{"cloud":"aws-snd","service":"sqs","type":"messageBrokers",
2042
+ "product":"my-product","component":"notifications-broker","env":"snd",
2043
+ "queueName":"my-product-notifications-snd"},
2044
+ {"cloud":"aws-prd","service":"sqs","type":"messageBrokers",
2045
+ "product":"my-product","component":"notifications-broker","env":"prd",
2046
+ "queueName":"my-product-notifications-prd"}]
2047
+
2048
+ Azure Service Bus — creates a namespace + queue per env:
2049
+ [{"cloud":"azure-snd","service":"servicebus","type":"messageBrokers",
2050
+ "product":"my-product","component":"notifications-broker","env":"snd",
2051
+ "namespaceName":"myproduct-snd","queueName":"notifications"},
2052
+ {"cloud":"azure-prd","service":"servicebus","type":"messageBrokers",
2053
+ "product":"my-product","component":"notifications-broker","env":"prd",
2054
+ "namespaceName":"myproduct-prd","queueName":"notifications"}]
2055
+
2056
+ Import (register an EXISTING cloud resource):
2057
+ Same as above but use import-persist-all and supply "resource" (the existing resource name/ID):
2058
+ ductape_cli("cloud resources import-persist-all -f brokers.json --json")
2059
+ Each entry: { cloud, service, type: "messageBrokers", product, component, env, resource: "<id>" }
2060
+
2061
+ IMPORTANT: Never share one cloud resource (topic/queue) across snd and prd envs — use
2062
+ separate resources per env to avoid mixing sandbox and production events.
2063
+
2064
+ ━━━ STEP 1B: REGISTER SELF-HOSTED BROKER (no cloud connection needed) ━━━
2065
+
2066
+ ductape_cli("resources events create -f broker.json")
2067
+ File: {
2068
+ name: string, tag: string, description?: string,
2069
+ envs: [
2070
+ {
2071
+ slug: "snd",
2072
+ type: "kafka"|"rabbitmq"|"redis"|"nats",
2073
+ config: <see config shapes below>
2074
+ },
2075
+ { slug: "prd", type: "kafka", config: { ... } }
2076
+ ]
2077
+ }
2078
+
2079
+ Config shapes per type:
2080
+ kafka: { brokers: ["host:9092"], clientId: "my-app", groupId?: "...",
2081
+ ssl?: true, sasl?: { mechanism: "plain", username, password } }
2082
+ rabbitmq: { url: "amqp://user:pass@host:5672/vhost" }
2083
+ redis: { host: "...", port: 6379, password?: "..." }
2084
+ nats: { servers: ["nats://host:4222"], token?: "...", user?: "...", pass?: "...", tls?: true }
2085
+
2086
+ ━━━ STEP 2: ADD TOPIC DEFINITIONS (all broker types — add as many as needed) ━━━
2087
+
2088
+ ductape_execute("messageBrokers.topics.create", [product_tag, {
2089
+ tag: "player-joined",
2090
+ name: "Player Joined",
2091
+ broker: "notifications-broker", // broker component tag
2092
+ description?: string,
2093
+ sample: { playerId: "string", username: "string" }, // example message shape
2094
+ idempotent?: boolean,
2095
+ // SQS only — map per-env queue URLs (each topic can be a different queue):
2096
+ queueUrls?: [
2097
+ { env_slug: "snd", url: "https://sqs.us-east-1.amazonaws.com/123456/my-product-player-joined-snd" },
2098
+ { env_slug: "prd", url: "https://sqs.us-east-1.amazonaws.com/123456/my-product-player-joined-prd" }
2099
+ ]
2100
+ }])
2101
+
2102
+ For GCP Pub/Sub and Azure Service Bus, the SDK resolves the topic name from the topic tag (or
2103
+ config.topicName on the env config). No extra per-topic URL mapping is needed beyond the tag.
2104
+ For SQS, every topic definition needs queueUrls to point to the specific per-env queue.
2105
+ Repeat this call for each logical event type — there is no limit on number of topics.
2106
+
2107
+ List topics on a broker:
2108
+ ductape_execute("messageBrokers.topics.list", [product_tag, "broker-tag"])
2109
+
2110
+ ━━━ RUNTIME: PRODUCE AND CONSUME ━━━
2111
+
2112
+ Produce (publish a message):
2113
+ → CALL ductape_generate_payload FIRST (operation_family="messaging", method="produce")
2114
+ messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value } }]
2115
+ Idempotent publish (deduplicates by key, default TTL 86400 s):
2116
+ messageBrokers.publishIdempotent [{ product, env, event, message, idempotency_key, ttl? }]
2117
+
2118
+ Consume (subscribe):
2119
+ messageBrokers.consume [{ product, env, event: "broker_tag:topic_tag",
2120
+ callback: async (message) => { ... } }]
2121
+ Callback errors are re-thrown so the broker can nack/retry.
2122
+
2123
+ Background dispatch with scheduling:
2124
+ messageBrokers.dispatch [{ product, env, broker, event, input: { message },
2125
+ schedule?: { start_at?, cron?, every?, limit?, endDate?, tz? } }]
2126
+ → CALL ductape_generate_payload FIRST (operation_family="messaging", method="dispatch")
2127
+
2128
+ Event format string: "broker_tag:topic_tag" — always colon-separated.
2129
+ Message payload is AES-encrypted before the tracking API call — tracking never sees plaintext.
2130
+
2131
+ ━━━ OBSERVABILITY ━━━
2132
+
2133
+ messageBrokers.messages.query [{ product, env, brokerTag, topicTag?, status?, page?, limit? }]
2134
+ messageBrokers.messages.getStats [{ product, env, brokerTag }]
2135
+ messageBrokers.messages.getDashboard [{ product, env, brokerTag }]
2136
+ messageBrokers.messages.getDeadLetters [{ product, env, brokerTag, topicTag?, limit? }]
2137
+ messageBrokers.replayEvent [{ product, env, eventId, force? }]
2138
+ messageBrokers.reprocessDLQ [{ product, env, brokerTag, topicTag?, messageIds?, limit? }]
2139
+ messageBrokers.checkIdempotency [{ product, env, brokerTag, idempotency_key }]
2140
+ `.trim(),
2141
+
2142
+ logs: `
2143
+ DUCTAPE LOGS
2144
+
2145
+ Logs record every SDK operation — actions, features, database queries, notifications, sessions,
2146
+ brokers, storage, cache hits, graph, vector, and resilience events.
2147
+
2148
+ Fetch logs (runtime — ductape_execute):
2149
+ ductape_execute("logs.fetch", [{
2150
+ app_id?: string, // query by app (requires component: "app")
2151
+ product_id?: string, // query by product (requires component: "product")
2152
+ env?: string,
2153
+ start_date?: string, // ISO 8601
2154
+ end_date?: string,
2155
+ page?: number,
2156
+ limit?: number,
2157
+ }])
2158
+ component "app" or "product" is required — there is no default.
2159
+ When component="app", tag is only valid with type: "actions".
2160
+
2161
+ Log data fields (ILogData):
2162
+ process_id, product_tag, env, type (LogEventType), status, data (encrypted if private key set)
2163
+ message?, parent_tag?, child_tag?, app_id?, action?, method?,
2164
+ cache_tag?, cache_key?, cache_status (true = hit),
2165
+ start, end, latency (auto-calculated from start/end),
2166
+ session_user_id?, session_id?, session_tag?, visitor_id?,
2167
+ ip_address?, language?, data_encrypted?, successful_execution?, failed_execution?
2168
+
2169
+ Log event types (LogEventTypes):
2170
+ notifications | push | email | sms | callbacks | slack | discord |
2171
+ database_actions | actions | functions | storage | webhook | jobs |
2172
+ message_broker | producer | consumer | quota | fallback | database_migration |
2173
+ feature | feature_step | database | graph | session | vector | cache | frontend
2174
+
2175
+ Log statuses: success | fail | waiting | processing
2176
+
2177
+ Encryption:
2178
+ When workspace_private_key is provided at SDK init, each log entry's data field is
2179
+ AES-encrypted client-side before transmission. data_encrypted: true is set on the entry.
2180
+ The backend can decrypt when returning logs to the Workbench.
2181
+
2182
+ Emit a log manually (SDK only — not via MCP):
2183
+ logs.add({ process_id, product_tag, env, type, status, data, message?, ... })
2184
+ logs.publish() → flushes all buffered entries to the API in one call
2185
+
2186
+ Notes:
2187
+ - Every SDK service (storage, broker, feature, graph, notifications, sessions, vector, cache,
2188
+ resilience) emits its own logs automatically — manual add/publish is only needed for custom logs.
2189
+ - language defaults to "typescript" so the backend knows which SDK emitted the entry.
2190
+ - Logs are batched in memory and sent in a single publish() call per operation.
2191
+ `.trim(),
2192
+ };
2193
+
2194
+ const docsHandler = async (args: { topic: string }) => {
2195
+ const key = args.topic.toLowerCase().trim();
2196
+ const doc = DOCS[key];
2197
+ if (!doc) {
2198
+ const available = Object.keys(DOCS).join(', ');
2199
+ return {
2200
+ content: [{ type: 'text' as const, text: `Unknown topic "${args.topic}". Available topics: ${available}` }],
2201
+ };
2202
+ }
2203
+ return { content: [{ type: 'text' as const, text: doc }] };
2204
+ };
2205
+
962
2206
  const cliInputSchema = z.object({
963
2207
  command: z.string().describe(
964
2208
  'The ductape CLI command to run, without the leading "ductape" word. ' +
@@ -1245,6 +2489,22 @@ async function main() {
1245
2489
  },
1246
2490
  schemaHandler,
1247
2491
  );
2492
+ server.registerTool(
2493
+ 'ductape_docs',
2494
+ {
2495
+ title: 'Ductape Feature Docs',
2496
+ description:
2497
+ 'Look up detailed documentation for a specific Ductape SDK feature.\n\n' +
2498
+ 'Call this when you need guidance on how a feature works before using it — ' +
2499
+ 'especially for features that require configuration decisions (tier, isolation level, ' +
2500
+ 'index strategy, operation types) that should be confirmed with the user first.\n\n' +
2501
+ 'Available topics: transactions, presave, triggers, aggregations, migrations, indexes, performance, actions, ' +
2502
+ 'graphs, storage, cloud, vector, warehouse, secrets, apps, products, sessions, caches, ' +
2503
+ 'notifications, resilience, features, events, logs',
2504
+ inputSchema: docsInputSchema,
2505
+ },
2506
+ docsHandler,
2507
+ );
1248
2508
  server.registerTool(
1249
2509
  'ductape_cli',
1250
2510
  {
@@ -1306,6 +2566,32 @@ async function main() {
1306
2566
  ' Supported service/component pairs include: rds→database, aurora→database, gcs→storage, s3→storage,\n' +
1307
2567
  ' azure-blob→storage, cloud-sql→database, neptune→graph, opensearch→vector. Attempting an unsupported pair\n' +
1308
2568
  ' will return an error; do not retry with a different tier — report the limitation to the user.\n' +
2569
+ ' - Atlas (MongoDB Atlas) import flow — service identifier is "atlas-cluster" (required, not optional):\n' +
2570
+ ' Step 1 — discover the cluster name:\n' +
2571
+ ' ductape_cli("cloud resources list -f /tmp/atlas-list.json --json")\n' +
2572
+ ' File: {"cloud": "<atlas-connection-tag>", "service": "atlas-cluster"}\n' +
2573
+ ' Returns a list of clusters; note the "name" field (this is your resource identifier).\n' +
2574
+ ' Step 2 — check if the database component already exists:\n' +
2575
+ ' ductape_cli("resources databases list <product_tag> --json")\n' +
2576
+ ' If a component already uses the same Atlas cluster, do NOT re-import — instead update it:\n' +
2577
+ ' ductape_execute("databases.updateDatabase", [product_tag, db_tag, { envs: [...updated envs...] }])\n' +
2578
+ ' Add or change the dbName in the env\'s connection_url to switch databases on the same cluster.\n' +
2579
+ ' Step 3 — import (only if no existing component uses this cluster):\n' +
2580
+ ' Use import-persist-all with one entry per product env. Required fields per entry:\n' +
2581
+ ' cloud (connection tag), service: "atlas-cluster", type: "databases",\n' +
2582
+ ' product, component (new tag), env, resource (cluster name from Step 1),\n' +
2583
+ ' dbName (the MongoDB database name to connect to — required for Atlas).\n' +
2584
+ ' Example: [{"cloud":"atlas-tag","service":"atlas-cluster","type":"databases",\n' +
2585
+ ' "product":"my-product","component":"core-db","env":"snd","resource":"Cluster0","dbName":"myapp_snd"},\n' +
2586
+ ' {"cloud":"atlas-tag","service":"atlas-cluster","type":"databases",\n' +
2587
+ ' "product":"my-product","component":"core-db","env":"prd","resource":"Cluster0","dbName":"myapp_prd"}]\n' +
2588
+ ' - Message broker / event broker import:\n' +
2589
+ ' CLI accepts these aliases for the messageBrokers module: events, event, broker, brokers, message-brokers.\n' +
2590
+ ' List existing brokers: ductape_cli("resources events list <product_tag> --json")\n' +
2591
+ ' GCP Pub/Sub service identifier is "pubsub". AWS SQS is "sqs". Azure Service Bus is "servicebus".\n' +
2592
+ ' Message brokers are import-only (no provision-persist). Import flow is the same as storage.\n' +
2593
+ ' type field = "messageBrokers" (not "messagebrokers" or "events").\n' +
2594
+ ' After importing, create topics via: ductape_execute("messageBrokers.topics.create", [product_tag, data])\n' +
1309
2595
  ' - Listing workspaces, products, secrets\n' +
1310
2596
  ' - Linking a project folder: "link --product <tag> --env <slug>"\n' +
1311
2597
  ' - Syncing sessions/notifications/events: "apply" or "apply sessions" etc.\n' +
@@ -1325,6 +2611,7 @@ async function main() {
1325
2611
  server.tool('ductape_generate_payload', payloadGenerateInputSchema.shape, payloadGenerateHandler);
1326
2612
  server.tool('ductape_generate_snippet', snippetGenerateInputSchema.shape, snippetGenerateHandler);
1327
2613
  server.tool('ductape_schema', schemaInputSchema.shape, schemaHandler);
2614
+ server.tool('ductape_docs', docsInputSchema.shape, docsHandler);
1328
2615
  server.tool('ductape_cli', cliInputSchema.shape, cliHandler);
1329
2616
  } else {
1330
2617
  console.error('MCP server does not expose .registerTool() or .tool()');