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