@ductape/mcp 0.1.23 → 0.1.25

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 +178 -2
  2. package/package.json +1 -1
  3. package/src/index.ts +178 -2
package/dist/index.js CHANGED
@@ -54,6 +54,67 @@ There are THREE categories of operations. Use the right tool for each:
54
54
  ductape_cli("db migrate rollback") # roll back last migration
55
55
  ductape_cli("db migrate rollback -n 3") # roll back last 3 migrations
56
56
 
57
+ DATABASE SCHEMA FILE — ductape/database/schema.json
58
+ ─────────────────────────────────────────────────────────────────────────
59
+ IMPORTANT: "db schema generate" READS schema.json and emits migration files.
60
+ It does NOT create schema.json from scratch. You must write this file first.
61
+
62
+ If schema.json does not exist, "db schema generate" now auto-creates it and
63
+ attempts to hydrate it from the cloud (pass --db <tag> to enable cloud hydration):
64
+ ductape_cli("db schema generate --db statecraft-core")
65
+ If the cloud database is empty, schema.json will be created as [] and you must
66
+ populate it before running the command again.
67
+
68
+ FILE FORMAT — ductape/database/schema.json is a JSON array:
69
+ [
70
+ {
71
+ "db": "<database-component-tag>",
72
+ "tables": {
73
+ "<collection_name>": {
74
+ "<field_name>": {
75
+ "type": "string|number|boolean|date|datetime|timestamp|uuid|object|array|json|text|decimal|bigint|smallint|double|binary|blob|time",
76
+ "required": true, // false by default (nullable)
77
+ "unique": true, // unique constraint on this field
78
+ "index": true, // create an index on this field (use with unique for unique index)
79
+ "default": "value", // default value ("now"/"NOW" → CURRENT_TIMESTAMP)
80
+ "maxlength": 255, // max length for string fields
81
+ "enum": ["a", "b", "c"], // restrict to enum values (sets type to string automatically)
82
+ "primaryKey": true, // mark as primary key
83
+ "autoGenerate": true, // auto-generate value on insert
84
+ "float": true // treat number as float (use with type: "number")
85
+ }
86
+ }
87
+ }
88
+ }
89
+ ]
90
+
91
+ SINGLE-FIELD INDEX: set index: true (+ unique: true for unique constraint)
92
+ "matchId": { "type": "string", "required": true, "unique": true, "index": true }
93
+
94
+ COMPOSITE INDEX: schema.json does not support composite indexes.
95
+ After running db schema generate, add a manual migration file in
96
+ ductape/migrations/<db-tag>/ with type "createIndex" and a fields array:
97
+ {
98
+ "tag": "create_map_events_match_seq_idx",
99
+ "name": "Composite unique index on matchId+sequence",
100
+ "up": [{
101
+ "type": "createIndex",
102
+ "collection": "map_events",
103
+ "name": "map_events_matchId_sequence_idx",
104
+ "fields": [{ "name": "matchId" }, { "name": "sequence" }],
105
+ "unique": true,
106
+ "ifNotExists": true
107
+ }],
108
+ "down": [{ "type": "dropIndex", "collection": "map_events", "name": "map_events_matchId_sequence_idx", "ifExists": true }],
109
+ "createdAt": "<ISO timestamp>"
110
+ }
111
+
112
+ WORKFLOW:
113
+ 1. Create/edit ductape/database/schema.json with your collection definitions
114
+ 2. ductape_cli("db schema generate --db <tag>") → writes migration files
115
+ 3. ductape_cli("db migrate --db <tag>") → applies pending migrations
116
+ 4. Add manual migration files for composite/partial indexes not expressible in schema.json
117
+
57
118
  1. ASSET CREATION / UPDATE (create, update, add, register… for ANY asset type)
58
119
  ALL creation and update operations require an access key and CANNOT go through ductape_execute.
59
120
  → Use ductape_cli for every create/update operation. Examples:
@@ -111,6 +172,107 @@ There are THREE categories of operations. Use the right tool for each:
111
172
 
112
173
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
113
174
 
175
+ ━━━ NESTJS INTEGRATION — use @ductape/nestjs ━━━
176
+
177
+ When the target application is a NestJS service or controller, always use @ductape/nestjs
178
+ instead of instantiating @ductape/sdk directly. It provides NestJS DI integration,
179
+ global interceptors, decorators, and type-safe resource handles.
180
+
181
+ SETUP — register once in AppModule:
182
+
183
+ import { DuctapeModule } from '@ductape/nestjs';
184
+
185
+ @Module({
186
+ imports: [
187
+ DuctapeModule.forIntegration({
188
+ accessKey: process.env.DUCTAPE_ACCESS_KEY,
189
+ product: 'my-product', // optional default — overridable per controller
190
+ env: process.env.NODE_ENV === 'production' ? 'prd' : 'snd',
191
+ }),
192
+ ],
193
+ })
194
+ export class AppModule {}
195
+
196
+ // Async (e.g. pulling key from ConfigService):
197
+ DuctapeModule.forRootAsync({
198
+ imports: [ConfigModule],
199
+ inject: [ConfigService],
200
+ useFactory: (cfg: ConfigService) => ({
201
+ accessKey: cfg.get('DUCTAPE_ACCESS_KEY'),
202
+ product: cfg.get('DUCTAPE_PRODUCT'),
203
+ env: cfg.get('DUCTAPE_ENV'),
204
+ }),
205
+ })
206
+
207
+ INJECTING IN SERVICES AND CONTROLLERS — use @InjectContext():
208
+
209
+ import { InjectContext, DuctapeContext } from '@ductape/nestjs';
210
+
211
+ @Injectable()
212
+ export class MatchService {
213
+ constructor(@InjectContext() private readonly ductape: DuctapeContext) {}
214
+
215
+ async publishMatchState(matchId: string, state: object) {
216
+ await this.ductape.sdk.events.produce({
217
+ product: 'my-product',
218
+ env: 'prd',
219
+ event: 'statecraft-events:match-state',
220
+ message: { matchId, ...state },
221
+ });
222
+ }
223
+
224
+ async queryDatabase(env: string) {
225
+ const db = await this.ductape.database('core-db', { env });
226
+ return db.query({ table: 'matches', where: { active: true } });
227
+ }
228
+ }
229
+
230
+ AVAILABLE ON DuctapeContext:
231
+
232
+ ctx.sdk — full @ductape/sdk instance (events, sessions, etc.)
233
+ ctx.database(tag, overrides?) — connected DatabaseHandle (query, insert, update, delete)
234
+ ctx.storage(tag, overrides?) — StorageHandle (upload, download, remove)
235
+ ctx.cache(tag, overrides?) — CacheHandle (get, set, delete)
236
+ ctx.graph(tag, overrides?) — GraphHandle
237
+ ctx.vector(tag, overrides?) — VectorHandle
238
+ ctx.agent(tag, overrides?) — AgentHandle
239
+ ctx.warehouse(overrides?) — WarehouseHandle
240
+ ctx.cloudTiers(query?) — query available cloud resource tiers and pricing
241
+ ctx.runJob({ product, env, event, input? }) — trigger a product job
242
+
243
+ CONTROLLER DECORATORS:
244
+
245
+ @Product('my-product') — override default product at class or method level
246
+ @Env('prd') — override default env
247
+ @ApiRun({ app, action }) — bind a controller method to an app action (interceptor runs it automatically)
248
+ @ApiConfig({ app, ... }) — shared app credentials at class level
249
+ @Webhook.Register(...) — register a webhook consumer URL
250
+ @Webhook.Consumer(...) — mark inbound handler for forwarded webhook payloads
251
+
252
+ FOR MESSAGING (events.produce / events.consume):
253
+
254
+ Access via ctx.sdk.events — no dedicated NestJS handle, the SDK instance is sufficient:
255
+
256
+ await this.ductape.sdk.events.produce({
257
+ product: 'my-product', env: 'prd',
258
+ event: 'broker-tag:topic-tag',
259
+ message: { ... },
260
+ });
261
+
262
+ await this.ductape.sdk.events.consume({
263
+ product: 'my-product', env: 'prd',
264
+ event: 'broker-tag:topic-tag',
265
+ callback: async (message) => { /* handle */ },
266
+ });
267
+
268
+ SPECIALIZED MODULES (for injecting handles directly without @InjectContext):
269
+
270
+ DuctapeDatabaseModule.forTags(['core-db']) → inject with @Database('core-db')
271
+ DuctapeStorageModule.forTags(['assets']) → inject with @Storage('assets')
272
+ DuctapeCacheModule, DuctapeGraphModule, DuctapeVectorModule — same pattern
273
+
274
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
275
+
114
276
  ALL params are passed as a JSON array in positional order matching the SDK signature.
115
277
 
116
278
  ━━━ MODULE: product ━━━
@@ -340,11 +502,23 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
340
502
  storage.dispatch [{ product, env, storage, operation, input, retries?, session?, cache?, schedule? }] ← CALL ductape_generate_payload FIRST (operation_family="storage", method="dispatch", targets={storage})
341
503
 
342
504
  ━━━ MODULE: databases ━━━
343
- databases.create [{ product, tag, name, description?, type: "mongodb"|"postgresql"|"mysql"|"sqlite", envs: [{slug, connection_url}] }]
505
+ databases.create [{ product, tag, name, description?, type: "mongodb"|"postgresql"|"mysql"|"sqlite",
506
+ envs: [{ slug, connection_url?,
507
+ cloud?, ← workspace cloud-connection tag (omit connection_url when using this)
508
+ authMode?, ← "cloud_connection" when using cloud field
509
+ dbName?, ← REQUIRED for MongoDB cloud connections — the database name inside the cluster (e.g. "myapp_snd")
510
+ instance?, ← RDS/Cloud SQL instance id (provision flow only)
511
+ region? }] }]
512
+ MONGODB CLOUD CONNECTION RULE: when type="mongodb" and cloud is set, dbName is MANDATORY.
513
+ Without dbName the Atlas API returns a URL with no database name and the adapter will throw
514
+ at migrate/connect time. Provide one dbName per env — they typically differ per environment:
515
+ e.g. { slug:"snd", cloud:"atlas-conn", authMode:"cloud_connection", dbName:"myapp_snd" }
516
+ { slug:"prd", cloud:"atlas-conn", authMode:"cloud_connection", dbName:"myapp_prd" }
344
517
  databases.register [product_tag, data: IProductDatabase]
345
518
  databases.list [product_tag]
346
519
  databases.fetch [product_tag, database_tag]
347
- databases.updateDatabase [product_tag, database_tag, data: { name?: string, description?: string, type?: "postgresql"|"mongodb"|"mysql", envs?: [{ slug: string, connection_url: string, description?: string }] }]
520
+ databases.updateDatabase [product_tag, database_tag, data: { name?, description?, type?,
521
+ envs?: [{ slug, connection_url?, cloud?, authMode?, dbName?, instance?, region? }] }]
348
522
  databases.connect [{ product, env, database }]
349
523
  databases.testConnection [{ product, env, database }]
350
524
  databases.disconnect []
@@ -2490,6 +2664,8 @@ async function main() {
2490
2664
  ' azure-blob→storage, cloud-sql→database, neptune→graph, opensearch→vector. Attempting an unsupported pair\n' +
2491
2665
  ' will return an error; do not retry with a different tier — report the limitation to the user.\n' +
2492
2666
  ' - Atlas (MongoDB Atlas) import flow — service identifier is "atlas-cluster" (required, not optional):\n' +
2667
+ ' IMPORTANT: dbName is mandatory for every MongoDB env — see "MONGODB CLOUD CONNECTION RULE" in the\n' +
2668
+ ' databases module above. The SDK enforces this at create time; omitting it throws a validation error.\n' +
2493
2669
  ' Step 1 — discover the cluster name:\n' +
2494
2670
  ' ductape_cli("cloud resources list -f /tmp/atlas-list.json --json")\n' +
2495
2671
  ' File: {"cloud": "<atlas-connection-tag>", "service": "atlas-cluster"}\n' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/mcp",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "MCP server that exposes Ductape SDK operations via the backend proxy",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/index.ts CHANGED
@@ -65,6 +65,67 @@ There are THREE categories of operations. Use the right tool for each:
65
65
  ductape_cli("db migrate rollback") # roll back last migration
66
66
  ductape_cli("db migrate rollback -n 3") # roll back last 3 migrations
67
67
 
68
+ DATABASE SCHEMA FILE — ductape/database/schema.json
69
+ ─────────────────────────────────────────────────────────────────────────
70
+ IMPORTANT: "db schema generate" READS schema.json and emits migration files.
71
+ It does NOT create schema.json from scratch. You must write this file first.
72
+
73
+ If schema.json does not exist, "db schema generate" now auto-creates it and
74
+ attempts to hydrate it from the cloud (pass --db <tag> to enable cloud hydration):
75
+ ductape_cli("db schema generate --db statecraft-core")
76
+ If the cloud database is empty, schema.json will be created as [] and you must
77
+ populate it before running the command again.
78
+
79
+ FILE FORMAT — ductape/database/schema.json is a JSON array:
80
+ [
81
+ {
82
+ "db": "<database-component-tag>",
83
+ "tables": {
84
+ "<collection_name>": {
85
+ "<field_name>": {
86
+ "type": "string|number|boolean|date|datetime|timestamp|uuid|object|array|json|text|decimal|bigint|smallint|double|binary|blob|time",
87
+ "required": true, // false by default (nullable)
88
+ "unique": true, // unique constraint on this field
89
+ "index": true, // create an index on this field (use with unique for unique index)
90
+ "default": "value", // default value ("now"/"NOW" → CURRENT_TIMESTAMP)
91
+ "maxlength": 255, // max length for string fields
92
+ "enum": ["a", "b", "c"], // restrict to enum values (sets type to string automatically)
93
+ "primaryKey": true, // mark as primary key
94
+ "autoGenerate": true, // auto-generate value on insert
95
+ "float": true // treat number as float (use with type: "number")
96
+ }
97
+ }
98
+ }
99
+ }
100
+ ]
101
+
102
+ SINGLE-FIELD INDEX: set index: true (+ unique: true for unique constraint)
103
+ "matchId": { "type": "string", "required": true, "unique": true, "index": true }
104
+
105
+ COMPOSITE INDEX: schema.json does not support composite indexes.
106
+ After running db schema generate, add a manual migration file in
107
+ ductape/migrations/<db-tag>/ with type "createIndex" and a fields array:
108
+ {
109
+ "tag": "create_map_events_match_seq_idx",
110
+ "name": "Composite unique index on matchId+sequence",
111
+ "up": [{
112
+ "type": "createIndex",
113
+ "collection": "map_events",
114
+ "name": "map_events_matchId_sequence_idx",
115
+ "fields": [{ "name": "matchId" }, { "name": "sequence" }],
116
+ "unique": true,
117
+ "ifNotExists": true
118
+ }],
119
+ "down": [{ "type": "dropIndex", "collection": "map_events", "name": "map_events_matchId_sequence_idx", "ifExists": true }],
120
+ "createdAt": "<ISO timestamp>"
121
+ }
122
+
123
+ WORKFLOW:
124
+ 1. Create/edit ductape/database/schema.json with your collection definitions
125
+ 2. ductape_cli("db schema generate --db <tag>") → writes migration files
126
+ 3. ductape_cli("db migrate --db <tag>") → applies pending migrations
127
+ 4. Add manual migration files for composite/partial indexes not expressible in schema.json
128
+
68
129
  1. ASSET CREATION / UPDATE (create, update, add, register… for ANY asset type)
69
130
  ALL creation and update operations require an access key and CANNOT go through ductape_execute.
70
131
  → Use ductape_cli for every create/update operation. Examples:
@@ -122,6 +183,107 @@ There are THREE categories of operations. Use the right tool for each:
122
183
 
123
184
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
124
185
 
186
+ ━━━ NESTJS INTEGRATION — use @ductape/nestjs ━━━
187
+
188
+ When the target application is a NestJS service or controller, always use @ductape/nestjs
189
+ instead of instantiating @ductape/sdk directly. It provides NestJS DI integration,
190
+ global interceptors, decorators, and type-safe resource handles.
191
+
192
+ SETUP — register once in AppModule:
193
+
194
+ import { DuctapeModule } from '@ductape/nestjs';
195
+
196
+ @Module({
197
+ imports: [
198
+ DuctapeModule.forIntegration({
199
+ accessKey: process.env.DUCTAPE_ACCESS_KEY,
200
+ product: 'my-product', // optional default — overridable per controller
201
+ env: process.env.NODE_ENV === 'production' ? 'prd' : 'snd',
202
+ }),
203
+ ],
204
+ })
205
+ export class AppModule {}
206
+
207
+ // Async (e.g. pulling key from ConfigService):
208
+ DuctapeModule.forRootAsync({
209
+ imports: [ConfigModule],
210
+ inject: [ConfigService],
211
+ useFactory: (cfg: ConfigService) => ({
212
+ accessKey: cfg.get('DUCTAPE_ACCESS_KEY'),
213
+ product: cfg.get('DUCTAPE_PRODUCT'),
214
+ env: cfg.get('DUCTAPE_ENV'),
215
+ }),
216
+ })
217
+
218
+ INJECTING IN SERVICES AND CONTROLLERS — use @InjectContext():
219
+
220
+ import { InjectContext, DuctapeContext } from '@ductape/nestjs';
221
+
222
+ @Injectable()
223
+ export class MatchService {
224
+ constructor(@InjectContext() private readonly ductape: DuctapeContext) {}
225
+
226
+ async publishMatchState(matchId: string, state: object) {
227
+ await this.ductape.sdk.events.produce({
228
+ product: 'my-product',
229
+ env: 'prd',
230
+ event: 'statecraft-events:match-state',
231
+ message: { matchId, ...state },
232
+ });
233
+ }
234
+
235
+ async queryDatabase(env: string) {
236
+ const db = await this.ductape.database('core-db', { env });
237
+ return db.query({ table: 'matches', where: { active: true } });
238
+ }
239
+ }
240
+
241
+ AVAILABLE ON DuctapeContext:
242
+
243
+ ctx.sdk — full @ductape/sdk instance (events, sessions, etc.)
244
+ ctx.database(tag, overrides?) — connected DatabaseHandle (query, insert, update, delete)
245
+ ctx.storage(tag, overrides?) — StorageHandle (upload, download, remove)
246
+ ctx.cache(tag, overrides?) — CacheHandle (get, set, delete)
247
+ ctx.graph(tag, overrides?) — GraphHandle
248
+ ctx.vector(tag, overrides?) — VectorHandle
249
+ ctx.agent(tag, overrides?) — AgentHandle
250
+ ctx.warehouse(overrides?) — WarehouseHandle
251
+ ctx.cloudTiers(query?) — query available cloud resource tiers and pricing
252
+ ctx.runJob({ product, env, event, input? }) — trigger a product job
253
+
254
+ CONTROLLER DECORATORS:
255
+
256
+ @Product('my-product') — override default product at class or method level
257
+ @Env('prd') — override default env
258
+ @ApiRun({ app, action }) — bind a controller method to an app action (interceptor runs it automatically)
259
+ @ApiConfig({ app, ... }) — shared app credentials at class level
260
+ @Webhook.Register(...) — register a webhook consumer URL
261
+ @Webhook.Consumer(...) — mark inbound handler for forwarded webhook payloads
262
+
263
+ FOR MESSAGING (events.produce / events.consume):
264
+
265
+ Access via ctx.sdk.events — no dedicated NestJS handle, the SDK instance is sufficient:
266
+
267
+ await this.ductape.sdk.events.produce({
268
+ product: 'my-product', env: 'prd',
269
+ event: 'broker-tag:topic-tag',
270
+ message: { ... },
271
+ });
272
+
273
+ await this.ductape.sdk.events.consume({
274
+ product: 'my-product', env: 'prd',
275
+ event: 'broker-tag:topic-tag',
276
+ callback: async (message) => { /* handle */ },
277
+ });
278
+
279
+ SPECIALIZED MODULES (for injecting handles directly without @InjectContext):
280
+
281
+ DuctapeDatabaseModule.forTags(['core-db']) → inject with @Database('core-db')
282
+ DuctapeStorageModule.forTags(['assets']) → inject with @Storage('assets')
283
+ DuctapeCacheModule, DuctapeGraphModule, DuctapeVectorModule — same pattern
284
+
285
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
286
+
125
287
  ALL params are passed as a JSON array in positional order matching the SDK signature.
126
288
 
127
289
  ━━━ MODULE: product ━━━
@@ -351,11 +513,23 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
351
513
  storage.dispatch [{ product, env, storage, operation, input, retries?, session?, cache?, schedule? }] ← CALL ductape_generate_payload FIRST (operation_family="storage", method="dispatch", targets={storage})
352
514
 
353
515
  ━━━ MODULE: databases ━━━
354
- databases.create [{ product, tag, name, description?, type: "mongodb"|"postgresql"|"mysql"|"sqlite", envs: [{slug, connection_url}] }]
516
+ databases.create [{ product, tag, name, description?, type: "mongodb"|"postgresql"|"mysql"|"sqlite",
517
+ envs: [{ slug, connection_url?,
518
+ cloud?, ← workspace cloud-connection tag (omit connection_url when using this)
519
+ authMode?, ← "cloud_connection" when using cloud field
520
+ dbName?, ← REQUIRED for MongoDB cloud connections — the database name inside the cluster (e.g. "myapp_snd")
521
+ instance?, ← RDS/Cloud SQL instance id (provision flow only)
522
+ region? }] }]
523
+ MONGODB CLOUD CONNECTION RULE: when type="mongodb" and cloud is set, dbName is MANDATORY.
524
+ Without dbName the Atlas API returns a URL with no database name and the adapter will throw
525
+ at migrate/connect time. Provide one dbName per env — they typically differ per environment:
526
+ e.g. { slug:"snd", cloud:"atlas-conn", authMode:"cloud_connection", dbName:"myapp_snd" }
527
+ { slug:"prd", cloud:"atlas-conn", authMode:"cloud_connection", dbName:"myapp_prd" }
355
528
  databases.register [product_tag, data: IProductDatabase]
356
529
  databases.list [product_tag]
357
530
  databases.fetch [product_tag, database_tag]
358
- databases.updateDatabase [product_tag, database_tag, data: { name?: string, description?: string, type?: "postgresql"|"mongodb"|"mysql", envs?: [{ slug: string, connection_url: string, description?: string }] }]
531
+ databases.updateDatabase [product_tag, database_tag, data: { name?, description?, type?,
532
+ envs?: [{ slug, connection_url?, cloud?, authMode?, dbName?, instance?, region? }] }]
359
533
  databases.connect [{ product, env, database }]
360
534
  databases.testConnection [{ product, env, database }]
361
535
  databases.disconnect []
@@ -2616,6 +2790,8 @@ async function main() {
2616
2790
  ' azure-blob→storage, cloud-sql→database, neptune→graph, opensearch→vector. Attempting an unsupported pair\n' +
2617
2791
  ' will return an error; do not retry with a different tier — report the limitation to the user.\n' +
2618
2792
  ' - Atlas (MongoDB Atlas) import flow — service identifier is "atlas-cluster" (required, not optional):\n' +
2793
+ ' IMPORTANT: dbName is mandatory for every MongoDB env — see "MONGODB CLOUD CONNECTION RULE" in the\n' +
2794
+ ' databases module above. The SDK enforces this at create time; omitting it throws a validation error.\n' +
2619
2795
  ' Step 1 — discover the cluster name:\n' +
2620
2796
  ' ductape_cli("cloud resources list -f /tmp/atlas-list.json --json")\n' +
2621
2797
  ' File: {"cloud": "<atlas-connection-tag>", "service": "atlas-cluster"}\n' +