@ductape/mcp 0.1.13 → 0.1.14

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