@jaypie/dynamodb 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/entities.d.ts +16 -47
- package/dist/cjs/index.cjs +220 -137
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.ts +1 -2
- package/dist/cjs/mcp/admin/createTable.d.ts +8 -0
- package/dist/cjs/mcp/admin/dockerCompose.d.ts +12 -0
- package/dist/cjs/mcp/admin/index.d.ts +3 -0
- package/dist/cjs/mcp/admin/status.d.ts +9 -0
- package/dist/cjs/mcp/autoInit.d.ts +5 -0
- package/dist/cjs/mcp/index.cjs +1090 -0
- package/dist/cjs/mcp/index.cjs.map +1 -0
- package/dist/cjs/mcp/index.d.ts +17 -0
- package/dist/cjs/queries.d.ts +36 -25
- package/dist/esm/entities.d.ts +16 -47
- package/dist/esm/index.d.ts +1 -2
- package/dist/esm/index.js +221 -138
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/mcp/admin/createTable.d.ts +8 -0
- package/dist/esm/mcp/admin/dockerCompose.d.ts +12 -0
- package/dist/esm/mcp/admin/index.d.ts +3 -0
- package/dist/esm/mcp/admin/status.d.ts +9 -0
- package/dist/esm/mcp/autoInit.d.ts +5 -0
- package/dist/esm/mcp/index.d.ts +17 -0
- package/dist/esm/mcp/index.js +1084 -0
- package/dist/esm/mcp/index.js.map +1 -0
- package/dist/esm/queries.d.ts +36 -25
- package/package.json +16 -3
|
@@ -0,0 +1,1090 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var mcp = require('@jaypie/vocabulary/mcp');
|
|
4
|
+
var vocabulary = require('@jaypie/vocabulary');
|
|
5
|
+
var clientDynamodb = require('@aws-sdk/client-dynamodb');
|
|
6
|
+
var libDynamodb = require('@aws-sdk/lib-dynamodb');
|
|
7
|
+
var errors = require('@jaypie/errors');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_REGION$2 = "us-east-1";
|
|
10
|
+
const LOCAL_CREDENTIALS = {
|
|
11
|
+
accessKeyId: "local",
|
|
12
|
+
secretAccessKey: "local",
|
|
13
|
+
};
|
|
14
|
+
// Module-level state
|
|
15
|
+
let docClient = null;
|
|
16
|
+
let tableName = null;
|
|
17
|
+
/**
|
|
18
|
+
* Check if endpoint indicates local development mode
|
|
19
|
+
*/
|
|
20
|
+
function isLocalEndpoint(endpoint) {
|
|
21
|
+
if (!endpoint)
|
|
22
|
+
return false;
|
|
23
|
+
return endpoint.includes("127.0.0.1") || endpoint.includes("localhost");
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Initialize the DynamoDB client
|
|
27
|
+
* Must be called once at application startup before using query functions
|
|
28
|
+
*
|
|
29
|
+
* @param config - Client configuration
|
|
30
|
+
*/
|
|
31
|
+
function initClient(config) {
|
|
32
|
+
const { endpoint, region = DEFAULT_REGION$2 } = config;
|
|
33
|
+
// Auto-detect local mode and use dummy credentials
|
|
34
|
+
const credentials = config.credentials ??
|
|
35
|
+
(isLocalEndpoint(endpoint) ? LOCAL_CREDENTIALS : undefined);
|
|
36
|
+
const dynamoClient = new clientDynamodb.DynamoDBClient({
|
|
37
|
+
...(credentials && { credentials }),
|
|
38
|
+
...(endpoint && { endpoint }),
|
|
39
|
+
region,
|
|
40
|
+
});
|
|
41
|
+
docClient = libDynamodb.DynamoDBDocumentClient.from(dynamoClient, {
|
|
42
|
+
marshallOptions: {
|
|
43
|
+
removeUndefinedValues: true,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
tableName = config.tableName;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get the initialized DynamoDB Document Client
|
|
50
|
+
* @throws ConfigurationError if client has not been initialized
|
|
51
|
+
*/
|
|
52
|
+
function getDocClient() {
|
|
53
|
+
if (!docClient) {
|
|
54
|
+
throw new errors.ConfigurationError("DynamoDB client not initialized. Call initClient() first.");
|
|
55
|
+
}
|
|
56
|
+
return docClient;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get the configured table name
|
|
60
|
+
* @throws ConfigurationError if client has not been initialized
|
|
61
|
+
*/
|
|
62
|
+
function getTableName() {
|
|
63
|
+
if (!tableName) {
|
|
64
|
+
throw new errors.ConfigurationError("DynamoDB client not initialized. Call initClient() first.");
|
|
65
|
+
}
|
|
66
|
+
return tableName;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if the client has been initialized
|
|
70
|
+
*/
|
|
71
|
+
function isInitialized() {
|
|
72
|
+
return docClient !== null && tableName !== null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Primary markers
|
|
76
|
+
const SEPARATOR = "#"; // Composite key separator
|
|
77
|
+
// GSI names
|
|
78
|
+
const INDEX_ALIAS = "indexAlias";
|
|
79
|
+
const INDEX_CLASS = "indexClass";
|
|
80
|
+
const INDEX_OU = "indexOu";
|
|
81
|
+
const INDEX_TYPE = "indexType";
|
|
82
|
+
const INDEX_XID = "indexXid";
|
|
83
|
+
// Index suffixes for soft state
|
|
84
|
+
const ARCHIVED_SUFFIX = "#archived";
|
|
85
|
+
const DELETED_SUFFIX = "#deleted";
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Build the indexOu key for hierarchical queries
|
|
89
|
+
* @param ou - The organizational unit (APEX or "{parent.model}#{parent.id}")
|
|
90
|
+
* @param model - The entity model name
|
|
91
|
+
* @returns Composite key: "{ou}#{model}"
|
|
92
|
+
*/
|
|
93
|
+
function buildIndexOu(ou, model) {
|
|
94
|
+
return `${ou}${SEPARATOR}${model}`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Build the indexAlias key for human-friendly lookups
|
|
98
|
+
* @param ou - The organizational unit
|
|
99
|
+
* @param model - The entity model name
|
|
100
|
+
* @param alias - The human-friendly alias
|
|
101
|
+
* @returns Composite key: "{ou}#{model}#{alias}"
|
|
102
|
+
*/
|
|
103
|
+
function buildIndexAlias(ou, model, alias) {
|
|
104
|
+
return `${ou}${SEPARATOR}${model}${SEPARATOR}${alias}`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Build the indexClass key for category filtering
|
|
108
|
+
* @param ou - The organizational unit
|
|
109
|
+
* @param model - The entity model name
|
|
110
|
+
* @param recordClass - The category classification
|
|
111
|
+
* @returns Composite key: "{ou}#{model}#{class}"
|
|
112
|
+
*/
|
|
113
|
+
function buildIndexClass(ou, model, recordClass) {
|
|
114
|
+
return `${ou}${SEPARATOR}${model}${SEPARATOR}${recordClass}`;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Build the indexType key for type filtering
|
|
118
|
+
* @param ou - The organizational unit
|
|
119
|
+
* @param model - The entity model name
|
|
120
|
+
* @param type - The type classification
|
|
121
|
+
* @returns Composite key: "{ou}#{model}#{type}"
|
|
122
|
+
*/
|
|
123
|
+
function buildIndexType(ou, model, type) {
|
|
124
|
+
return `${ou}${SEPARATOR}${model}${SEPARATOR}${type}`;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Build the indexXid key for external ID lookups
|
|
128
|
+
* @param ou - The organizational unit
|
|
129
|
+
* @param model - The entity model name
|
|
130
|
+
* @param xid - The external ID
|
|
131
|
+
* @returns Composite key: "{ou}#{model}#{xid}"
|
|
132
|
+
*/
|
|
133
|
+
function buildIndexXid(ou, model, xid) {
|
|
134
|
+
return `${ou}${SEPARATOR}${model}${SEPARATOR}${xid}`;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Auto-populate GSI index keys on an entity
|
|
138
|
+
* - indexOu is always populated from ou + model
|
|
139
|
+
* - indexAlias is populated only when alias is present
|
|
140
|
+
* - indexClass is populated only when class is present
|
|
141
|
+
* - indexType is populated only when type is present
|
|
142
|
+
* - indexXid is populated only when xid is present
|
|
143
|
+
*
|
|
144
|
+
* @param entity - The entity to populate index keys for
|
|
145
|
+
* @param suffix - Optional suffix to append to all index keys (e.g., "#deleted", "#archived")
|
|
146
|
+
* @returns The entity with populated index keys
|
|
147
|
+
*/
|
|
148
|
+
function indexEntity(entity, suffix = "") {
|
|
149
|
+
const result = { ...entity };
|
|
150
|
+
// indexOu is always set (from ou + model)
|
|
151
|
+
result.indexOu = buildIndexOu(entity.ou, entity.model) + suffix;
|
|
152
|
+
// Optional indexes - only set when the source field is present
|
|
153
|
+
if (entity.alias !== undefined) {
|
|
154
|
+
result.indexAlias =
|
|
155
|
+
buildIndexAlias(entity.ou, entity.model, entity.alias) + suffix;
|
|
156
|
+
}
|
|
157
|
+
if (entity.class !== undefined) {
|
|
158
|
+
result.indexClass =
|
|
159
|
+
buildIndexClass(entity.ou, entity.model, entity.class) + suffix;
|
|
160
|
+
}
|
|
161
|
+
if (entity.type !== undefined) {
|
|
162
|
+
result.indexType =
|
|
163
|
+
buildIndexType(entity.ou, entity.model, entity.type) + suffix;
|
|
164
|
+
}
|
|
165
|
+
if (entity.xid !== undefined) {
|
|
166
|
+
result.indexXid =
|
|
167
|
+
buildIndexXid(entity.ou, entity.model, entity.xid) + suffix;
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Calculate suffix based on entity's archived/deleted state
|
|
174
|
+
*/
|
|
175
|
+
function calculateEntitySuffix(entity) {
|
|
176
|
+
const hasArchived = Boolean(entity.archivedAt);
|
|
177
|
+
const hasDeleted = Boolean(entity.deletedAt);
|
|
178
|
+
if (hasArchived && hasDeleted) {
|
|
179
|
+
return ARCHIVED_SUFFIX + DELETED_SUFFIX;
|
|
180
|
+
}
|
|
181
|
+
if (hasArchived) {
|
|
182
|
+
return ARCHIVED_SUFFIX;
|
|
183
|
+
}
|
|
184
|
+
if (hasDeleted) {
|
|
185
|
+
return DELETED_SUFFIX;
|
|
186
|
+
}
|
|
187
|
+
return "";
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Get a single entity by primary key
|
|
191
|
+
*/
|
|
192
|
+
const getEntity = vocabulary.serviceHandler({
|
|
193
|
+
alias: "getEntity",
|
|
194
|
+
description: "Get a single entity by primary key",
|
|
195
|
+
input: {
|
|
196
|
+
id: { type: String, description: "Entity ID (sort key)" },
|
|
197
|
+
model: { type: String, description: "Entity model (partition key)" },
|
|
198
|
+
},
|
|
199
|
+
service: async ({ id, model }) => {
|
|
200
|
+
const docClient = getDocClient();
|
|
201
|
+
const tableName = getTableName();
|
|
202
|
+
const command = new libDynamodb.GetCommand({
|
|
203
|
+
Key: { id, model },
|
|
204
|
+
TableName: tableName,
|
|
205
|
+
});
|
|
206
|
+
const response = await docClient.send(command);
|
|
207
|
+
return response.Item ?? null;
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
/**
|
|
211
|
+
* Put (create or replace) an entity
|
|
212
|
+
* Auto-populates GSI index keys via indexEntity
|
|
213
|
+
*
|
|
214
|
+
* Note: This is a regular async function (not serviceHandler) because it accepts
|
|
215
|
+
* complex FabricEntity objects that can't be coerced by vocabulary's type system.
|
|
216
|
+
*/
|
|
217
|
+
async function putEntity({ entity, }) {
|
|
218
|
+
const docClient = getDocClient();
|
|
219
|
+
const tableName = getTableName();
|
|
220
|
+
// Auto-populate index keys
|
|
221
|
+
const indexedEntity = indexEntity(entity);
|
|
222
|
+
const command = new libDynamodb.PutCommand({
|
|
223
|
+
Item: indexedEntity,
|
|
224
|
+
TableName: tableName,
|
|
225
|
+
});
|
|
226
|
+
await docClient.send(command);
|
|
227
|
+
return indexedEntity;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Update an existing entity
|
|
231
|
+
* Auto-populates GSI index keys and sets updatedAt
|
|
232
|
+
*
|
|
233
|
+
* Note: This is a regular async function (not serviceHandler) because it accepts
|
|
234
|
+
* complex FabricEntity objects that can't be coerced by vocabulary's type system.
|
|
235
|
+
*/
|
|
236
|
+
async function updateEntity({ entity, }) {
|
|
237
|
+
const docClient = getDocClient();
|
|
238
|
+
const tableName = getTableName();
|
|
239
|
+
// Update timestamp and re-index
|
|
240
|
+
const updatedEntity = indexEntity({
|
|
241
|
+
...entity,
|
|
242
|
+
updatedAt: new Date().toISOString(),
|
|
243
|
+
});
|
|
244
|
+
const command = new libDynamodb.PutCommand({
|
|
245
|
+
Item: updatedEntity,
|
|
246
|
+
TableName: tableName,
|
|
247
|
+
});
|
|
248
|
+
await docClient.send(command);
|
|
249
|
+
return updatedEntity;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Soft delete an entity by setting deletedAt timestamp
|
|
253
|
+
* Re-indexes with appropriate suffix based on archived/deleted state
|
|
254
|
+
*/
|
|
255
|
+
const deleteEntity = vocabulary.serviceHandler({
|
|
256
|
+
alias: "deleteEntity",
|
|
257
|
+
description: "Soft delete an entity (sets deletedAt timestamp)",
|
|
258
|
+
input: {
|
|
259
|
+
id: { type: String, description: "Entity ID (sort key)" },
|
|
260
|
+
model: { type: String, description: "Entity model (partition key)" },
|
|
261
|
+
},
|
|
262
|
+
service: async ({ id, model }) => {
|
|
263
|
+
const docClient = getDocClient();
|
|
264
|
+
const tableName = getTableName();
|
|
265
|
+
// Fetch the current entity
|
|
266
|
+
const existing = await getEntity({ id, model });
|
|
267
|
+
if (!existing) {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
const now = new Date().toISOString();
|
|
271
|
+
// Build updated entity with deletedAt
|
|
272
|
+
const updatedEntity = {
|
|
273
|
+
...existing,
|
|
274
|
+
deletedAt: now,
|
|
275
|
+
updatedAt: now,
|
|
276
|
+
};
|
|
277
|
+
// Calculate suffix based on combined state (may already be archived)
|
|
278
|
+
const suffix = calculateEntitySuffix(updatedEntity);
|
|
279
|
+
const deletedEntity = indexEntity(updatedEntity, suffix);
|
|
280
|
+
const command = new libDynamodb.PutCommand({
|
|
281
|
+
Item: deletedEntity,
|
|
282
|
+
TableName: tableName,
|
|
283
|
+
});
|
|
284
|
+
await docClient.send(command);
|
|
285
|
+
return true;
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
/**
|
|
289
|
+
* Archive an entity by setting archivedAt timestamp
|
|
290
|
+
* Re-indexes with appropriate suffix based on archived/deleted state
|
|
291
|
+
*/
|
|
292
|
+
const archiveEntity = vocabulary.serviceHandler({
|
|
293
|
+
alias: "archiveEntity",
|
|
294
|
+
description: "Archive an entity (sets archivedAt timestamp)",
|
|
295
|
+
input: {
|
|
296
|
+
id: { type: String, description: "Entity ID (sort key)" },
|
|
297
|
+
model: { type: String, description: "Entity model (partition key)" },
|
|
298
|
+
},
|
|
299
|
+
service: async ({ id, model }) => {
|
|
300
|
+
const docClient = getDocClient();
|
|
301
|
+
const tableName = getTableName();
|
|
302
|
+
// Fetch the current entity
|
|
303
|
+
const existing = await getEntity({ id, model });
|
|
304
|
+
if (!existing) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
const now = new Date().toISOString();
|
|
308
|
+
// Build updated entity with archivedAt
|
|
309
|
+
const updatedEntity = {
|
|
310
|
+
...existing,
|
|
311
|
+
archivedAt: now,
|
|
312
|
+
updatedAt: now,
|
|
313
|
+
};
|
|
314
|
+
// Calculate suffix based on combined state (may already be deleted)
|
|
315
|
+
const suffix = calculateEntitySuffix(updatedEntity);
|
|
316
|
+
const archivedEntity = indexEntity(updatedEntity, suffix);
|
|
317
|
+
const command = new libDynamodb.PutCommand({
|
|
318
|
+
Item: archivedEntity,
|
|
319
|
+
TableName: tableName,
|
|
320
|
+
});
|
|
321
|
+
await docClient.send(command);
|
|
322
|
+
return true;
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
/**
|
|
326
|
+
* Hard delete an entity (permanently removes from table)
|
|
327
|
+
* Use with caution - prefer deleteEntity for soft delete
|
|
328
|
+
*/
|
|
329
|
+
const destroyEntity = vocabulary.serviceHandler({
|
|
330
|
+
alias: "destroyEntity",
|
|
331
|
+
description: "Hard delete an entity (permanently removes from table)",
|
|
332
|
+
input: {
|
|
333
|
+
id: { type: String, description: "Entity ID (sort key)" },
|
|
334
|
+
model: { type: String, description: "Entity model (partition key)" },
|
|
335
|
+
},
|
|
336
|
+
service: async ({ id, model }) => {
|
|
337
|
+
const docClient = getDocClient();
|
|
338
|
+
const tableName = getTableName();
|
|
339
|
+
const command = new libDynamodb.DeleteCommand({
|
|
340
|
+
Key: { id, model },
|
|
341
|
+
TableName: tableName,
|
|
342
|
+
});
|
|
343
|
+
await docClient.send(command);
|
|
344
|
+
return true;
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Calculate the suffix based on archived/deleted flags
|
|
350
|
+
* When both are true, returns combined suffix (archived first, alphabetically)
|
|
351
|
+
*/
|
|
352
|
+
function calculateSuffix({ archived, deleted, }) {
|
|
353
|
+
if (archived && deleted) {
|
|
354
|
+
return ARCHIVED_SUFFIX + DELETED_SUFFIX;
|
|
355
|
+
}
|
|
356
|
+
if (archived) {
|
|
357
|
+
return ARCHIVED_SUFFIX;
|
|
358
|
+
}
|
|
359
|
+
if (deleted) {
|
|
360
|
+
return DELETED_SUFFIX;
|
|
361
|
+
}
|
|
362
|
+
return "";
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Execute a GSI query with common options
|
|
366
|
+
*/
|
|
367
|
+
async function executeQuery(indexName, keyValue, options = {}) {
|
|
368
|
+
const { ascending = false, limit, startKey } = options;
|
|
369
|
+
const docClient = getDocClient();
|
|
370
|
+
const tableName = getTableName();
|
|
371
|
+
const command = new libDynamodb.QueryCommand({
|
|
372
|
+
ExclusiveStartKey: startKey,
|
|
373
|
+
ExpressionAttributeNames: {
|
|
374
|
+
"#pk": indexName,
|
|
375
|
+
},
|
|
376
|
+
ExpressionAttributeValues: {
|
|
377
|
+
":pkValue": keyValue,
|
|
378
|
+
},
|
|
379
|
+
IndexName: indexName,
|
|
380
|
+
KeyConditionExpression: "#pk = :pkValue",
|
|
381
|
+
...(limit && { Limit: limit }),
|
|
382
|
+
ScanIndexForward: ascending,
|
|
383
|
+
TableName: tableName,
|
|
384
|
+
});
|
|
385
|
+
const response = await docClient.send(command);
|
|
386
|
+
return {
|
|
387
|
+
items: (response.Items ?? []),
|
|
388
|
+
lastEvaluatedKey: response.LastEvaluatedKey,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Query entities by organizational unit (parent hierarchy)
|
|
393
|
+
* Uses indexOu GSI
|
|
394
|
+
*
|
|
395
|
+
* Note: This is a regular async function (not serviceHandler) because it accepts
|
|
396
|
+
* complex startKey objects that can't be coerced by vocabulary's type system.
|
|
397
|
+
*/
|
|
398
|
+
async function queryByOu({ archived = false, ascending = false, deleted = false, limit, model, ou, startKey, }) {
|
|
399
|
+
const suffix = calculateSuffix({ archived, deleted });
|
|
400
|
+
const keyValue = buildIndexOu(ou, model) + suffix;
|
|
401
|
+
return executeQuery(INDEX_OU, keyValue, {
|
|
402
|
+
ascending,
|
|
403
|
+
limit,
|
|
404
|
+
startKey,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Query a single entity by human-friendly alias
|
|
409
|
+
* Uses indexAlias GSI
|
|
410
|
+
*/
|
|
411
|
+
const queryByAlias = vocabulary.serviceHandler({
|
|
412
|
+
alias: "queryByAlias",
|
|
413
|
+
description: "Query a single entity by human-friendly alias",
|
|
414
|
+
input: {
|
|
415
|
+
alias: { type: String, description: "Human-friendly alias" },
|
|
416
|
+
archived: {
|
|
417
|
+
type: Boolean,
|
|
418
|
+
default: false,
|
|
419
|
+
required: false,
|
|
420
|
+
description: "Query archived entities instead of active ones",
|
|
421
|
+
},
|
|
422
|
+
deleted: {
|
|
423
|
+
type: Boolean,
|
|
424
|
+
default: false,
|
|
425
|
+
required: false,
|
|
426
|
+
description: "Query deleted entities instead of active ones",
|
|
427
|
+
},
|
|
428
|
+
model: { type: String, description: "Entity model name" },
|
|
429
|
+
ou: { type: String, description: "Organizational unit (@ for root)" },
|
|
430
|
+
},
|
|
431
|
+
service: async ({ alias, archived, deleted, model, ou, }) => {
|
|
432
|
+
const aliasStr = alias;
|
|
433
|
+
const archivedBool = archived;
|
|
434
|
+
const deletedBool = deleted;
|
|
435
|
+
const modelStr = model;
|
|
436
|
+
const ouStr = ou;
|
|
437
|
+
const suffix = calculateSuffix({ archived: archivedBool, deleted: deletedBool });
|
|
438
|
+
const keyValue = buildIndexAlias(ouStr, modelStr, aliasStr) + suffix;
|
|
439
|
+
const result = await executeQuery(INDEX_ALIAS, keyValue, {
|
|
440
|
+
limit: 1,
|
|
441
|
+
});
|
|
442
|
+
return result.items[0] ?? null;
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
/**
|
|
446
|
+
* Query entities by category classification
|
|
447
|
+
* Uses indexClass GSI
|
|
448
|
+
*
|
|
449
|
+
* Note: This is a regular async function (not serviceHandler) because it accepts
|
|
450
|
+
* complex startKey objects that can't be coerced by vocabulary's type system.
|
|
451
|
+
*/
|
|
452
|
+
async function queryByClass({ archived = false, ascending = false, deleted = false, limit, model, ou, recordClass, startKey, }) {
|
|
453
|
+
const suffix = calculateSuffix({ archived, deleted });
|
|
454
|
+
const keyValue = buildIndexClass(ou, model, recordClass) + suffix;
|
|
455
|
+
return executeQuery(INDEX_CLASS, keyValue, {
|
|
456
|
+
ascending,
|
|
457
|
+
limit,
|
|
458
|
+
startKey,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Query entities by type classification
|
|
463
|
+
* Uses indexType GSI
|
|
464
|
+
*
|
|
465
|
+
* Note: This is a regular async function (not serviceHandler) because it accepts
|
|
466
|
+
* complex startKey objects that can't be coerced by vocabulary's type system.
|
|
467
|
+
*/
|
|
468
|
+
async function queryByType({ archived = false, ascending = false, deleted = false, limit, model, ou, startKey, type, }) {
|
|
469
|
+
const suffix = calculateSuffix({ archived, deleted });
|
|
470
|
+
const keyValue = buildIndexType(ou, model, type) + suffix;
|
|
471
|
+
return executeQuery(INDEX_TYPE, keyValue, {
|
|
472
|
+
ascending,
|
|
473
|
+
limit,
|
|
474
|
+
startKey,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Query a single entity by external ID
|
|
479
|
+
* Uses indexXid GSI
|
|
480
|
+
*/
|
|
481
|
+
const queryByXid = vocabulary.serviceHandler({
|
|
482
|
+
alias: "queryByXid",
|
|
483
|
+
description: "Query a single entity by external ID",
|
|
484
|
+
input: {
|
|
485
|
+
archived: {
|
|
486
|
+
type: Boolean,
|
|
487
|
+
default: false,
|
|
488
|
+
required: false,
|
|
489
|
+
description: "Query archived entities instead of active ones",
|
|
490
|
+
},
|
|
491
|
+
deleted: {
|
|
492
|
+
type: Boolean,
|
|
493
|
+
default: false,
|
|
494
|
+
required: false,
|
|
495
|
+
description: "Query deleted entities instead of active ones",
|
|
496
|
+
},
|
|
497
|
+
model: { type: String, description: "Entity model name" },
|
|
498
|
+
ou: { type: String, description: "Organizational unit (@ for root)" },
|
|
499
|
+
xid: { type: String, description: "External ID" },
|
|
500
|
+
},
|
|
501
|
+
service: async ({ archived, deleted, model, ou, xid, }) => {
|
|
502
|
+
const archivedBool = archived;
|
|
503
|
+
const deletedBool = deleted;
|
|
504
|
+
const modelStr = model;
|
|
505
|
+
const ouStr = ou;
|
|
506
|
+
const xidStr = xid;
|
|
507
|
+
const suffix = calculateSuffix({ archived: archivedBool, deleted: deletedBool });
|
|
508
|
+
const keyValue = buildIndexXid(ouStr, modelStr, xidStr) + suffix;
|
|
509
|
+
const result = await executeQuery(INDEX_XID, keyValue, {
|
|
510
|
+
limit: 1,
|
|
511
|
+
});
|
|
512
|
+
return result.items[0] ?? null;
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const DEFAULT_ENDPOINT = "http://127.0.0.1:8000";
|
|
517
|
+
const DEFAULT_REGION$1 = "us-east-1";
|
|
518
|
+
const DEFAULT_TABLE_NAME$1 = "jaypie-local";
|
|
519
|
+
/**
|
|
520
|
+
* DynamoDB table schema with Jaypie GSI pattern
|
|
521
|
+
*/
|
|
522
|
+
function createTableParams(tableName, billingMode) {
|
|
523
|
+
const gsiProjection = { ProjectionType: "ALL" };
|
|
524
|
+
return {
|
|
525
|
+
AttributeDefinitions: [
|
|
526
|
+
{ AttributeName: "id", AttributeType: "S" },
|
|
527
|
+
{ AttributeName: "indexAlias", AttributeType: "S" },
|
|
528
|
+
{ AttributeName: "indexClass", AttributeType: "S" },
|
|
529
|
+
{ AttributeName: "indexOu", AttributeType: "S" },
|
|
530
|
+
{ AttributeName: "indexType", AttributeType: "S" },
|
|
531
|
+
{ AttributeName: "indexXid", AttributeType: "S" },
|
|
532
|
+
{ AttributeName: "model", AttributeType: "S" },
|
|
533
|
+
{ AttributeName: "sequence", AttributeType: "N" },
|
|
534
|
+
],
|
|
535
|
+
BillingMode: billingMode,
|
|
536
|
+
GlobalSecondaryIndexes: [
|
|
537
|
+
{
|
|
538
|
+
IndexName: "indexOu",
|
|
539
|
+
KeySchema: [
|
|
540
|
+
{ AttributeName: "indexOu", KeyType: "HASH" },
|
|
541
|
+
{ AttributeName: "sequence", KeyType: "RANGE" },
|
|
542
|
+
],
|
|
543
|
+
Projection: gsiProjection,
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
IndexName: "indexAlias",
|
|
547
|
+
KeySchema: [
|
|
548
|
+
{ AttributeName: "indexAlias", KeyType: "HASH" },
|
|
549
|
+
{ AttributeName: "sequence", KeyType: "RANGE" },
|
|
550
|
+
],
|
|
551
|
+
Projection: gsiProjection,
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
IndexName: "indexClass",
|
|
555
|
+
KeySchema: [
|
|
556
|
+
{ AttributeName: "indexClass", KeyType: "HASH" },
|
|
557
|
+
{ AttributeName: "sequence", KeyType: "RANGE" },
|
|
558
|
+
],
|
|
559
|
+
Projection: gsiProjection,
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
IndexName: "indexType",
|
|
563
|
+
KeySchema: [
|
|
564
|
+
{ AttributeName: "indexType", KeyType: "HASH" },
|
|
565
|
+
{ AttributeName: "sequence", KeyType: "RANGE" },
|
|
566
|
+
],
|
|
567
|
+
Projection: gsiProjection,
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
IndexName: "indexXid",
|
|
571
|
+
KeySchema: [
|
|
572
|
+
{ AttributeName: "indexXid", KeyType: "HASH" },
|
|
573
|
+
{ AttributeName: "sequence", KeyType: "RANGE" },
|
|
574
|
+
],
|
|
575
|
+
Projection: gsiProjection,
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
KeySchema: [
|
|
579
|
+
{ AttributeName: "model", KeyType: "HASH" },
|
|
580
|
+
{ AttributeName: "id", KeyType: "RANGE" },
|
|
581
|
+
],
|
|
582
|
+
TableName: tableName,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Create DynamoDB table with Jaypie GSI schema
|
|
587
|
+
*/
|
|
588
|
+
const createTableHandler = vocabulary.serviceHandler({
|
|
589
|
+
alias: "dynamodb_create_table",
|
|
590
|
+
description: "Create DynamoDB table with Jaypie GSI schema",
|
|
591
|
+
input: {
|
|
592
|
+
billingMode: {
|
|
593
|
+
type: ["PAY_PER_REQUEST", "PROVISIONED"],
|
|
594
|
+
default: "PAY_PER_REQUEST",
|
|
595
|
+
description: "DynamoDB billing mode",
|
|
596
|
+
},
|
|
597
|
+
endpoint: {
|
|
598
|
+
type: String,
|
|
599
|
+
default: DEFAULT_ENDPOINT,
|
|
600
|
+
description: "DynamoDB endpoint URL",
|
|
601
|
+
},
|
|
602
|
+
tableName: {
|
|
603
|
+
type: String,
|
|
604
|
+
default: DEFAULT_TABLE_NAME$1,
|
|
605
|
+
description: "Table name to create",
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
service: async ({ billingMode, endpoint, tableName }) => {
|
|
609
|
+
const endpointStr = endpoint;
|
|
610
|
+
const tableNameStr = tableName;
|
|
611
|
+
const billingModeStr = billingMode;
|
|
612
|
+
const client = new clientDynamodb.DynamoDBClient({
|
|
613
|
+
credentials: {
|
|
614
|
+
accessKeyId: "local",
|
|
615
|
+
secretAccessKey: "local",
|
|
616
|
+
},
|
|
617
|
+
endpoint: endpointStr,
|
|
618
|
+
region: DEFAULT_REGION$1,
|
|
619
|
+
});
|
|
620
|
+
try {
|
|
621
|
+
// Check if table already exists
|
|
622
|
+
await client.send(new clientDynamodb.DescribeTableCommand({ TableName: tableNameStr }));
|
|
623
|
+
return {
|
|
624
|
+
message: `Table "${tableNameStr}" already exists`,
|
|
625
|
+
success: false,
|
|
626
|
+
tableName: tableNameStr,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
if (error.name !== "ResourceNotFoundException") {
|
|
631
|
+
throw error;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
// Create the table
|
|
635
|
+
const tableParams = createTableParams(tableNameStr, billingModeStr);
|
|
636
|
+
await client.send(new clientDynamodb.CreateTableCommand(tableParams));
|
|
637
|
+
return {
|
|
638
|
+
message: "Table created successfully",
|
|
639
|
+
success: true,
|
|
640
|
+
tableName: tableNameStr,
|
|
641
|
+
};
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
const DEFAULT_ADMIN_PORT = 8001;
|
|
646
|
+
const DEFAULT_DYNAMODB_PORT = 8000;
|
|
647
|
+
const DEFAULT_PROJECT_NAME = "jaypie";
|
|
648
|
+
const DEFAULT_TABLE_NAME = "jaypie-local";
|
|
649
|
+
/**
|
|
650
|
+
* Generate docker-compose.yml for local DynamoDB development
|
|
651
|
+
*/
|
|
652
|
+
const dockerComposeHandler = vocabulary.serviceHandler({
|
|
653
|
+
alias: "dynamodb_generate_docker_compose",
|
|
654
|
+
description: "Generate docker-compose.yml for local DynamoDB development",
|
|
655
|
+
input: {
|
|
656
|
+
adminPort: {
|
|
657
|
+
type: Number,
|
|
658
|
+
default: DEFAULT_ADMIN_PORT,
|
|
659
|
+
description: "Port for DynamoDB Admin UI",
|
|
660
|
+
},
|
|
661
|
+
dynamodbPort: {
|
|
662
|
+
type: Number,
|
|
663
|
+
default: DEFAULT_DYNAMODB_PORT,
|
|
664
|
+
description: "Port for DynamoDB Local",
|
|
665
|
+
},
|
|
666
|
+
projectName: {
|
|
667
|
+
type: String,
|
|
668
|
+
default: DEFAULT_PROJECT_NAME,
|
|
669
|
+
description: "Project name for container naming",
|
|
670
|
+
},
|
|
671
|
+
tableName: {
|
|
672
|
+
type: String,
|
|
673
|
+
default: DEFAULT_TABLE_NAME,
|
|
674
|
+
description: "Default table name",
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
service: async ({ adminPort, dynamodbPort, projectName, tableName }) => {
|
|
678
|
+
const dockerCompose = `name: ${projectName}-dynamodb-stack
|
|
679
|
+
|
|
680
|
+
services:
|
|
681
|
+
dynamodb:
|
|
682
|
+
image: amazon/dynamodb-local:latest
|
|
683
|
+
container_name: ${projectName}-dynamodb
|
|
684
|
+
command: "-jar DynamoDBLocal.jar -sharedDb -dbPath /data"
|
|
685
|
+
ports:
|
|
686
|
+
- "${dynamodbPort}:8000"
|
|
687
|
+
working_dir: /home/dynamodblocal
|
|
688
|
+
volumes:
|
|
689
|
+
- dynamodb_data:/data
|
|
690
|
+
user: "0:0"
|
|
691
|
+
|
|
692
|
+
dynamodb-admin:
|
|
693
|
+
image: aaronshaf/dynamodb-admin:latest
|
|
694
|
+
container_name: ${projectName}-dynamodb-admin
|
|
695
|
+
ports:
|
|
696
|
+
- "${adminPort}:8001"
|
|
697
|
+
environment:
|
|
698
|
+
- DYNAMO_ENDPOINT=http://dynamodb:8000
|
|
699
|
+
- AWS_REGION=us-east-1
|
|
700
|
+
- AWS_ACCESS_KEY_ID=local
|
|
701
|
+
- AWS_SECRET_ACCESS_KEY=local
|
|
702
|
+
depends_on:
|
|
703
|
+
- dynamodb
|
|
704
|
+
|
|
705
|
+
volumes:
|
|
706
|
+
dynamodb_data:
|
|
707
|
+
driver: local
|
|
708
|
+
`;
|
|
709
|
+
const envVars = {
|
|
710
|
+
AWS_REGION: "us-east-1",
|
|
711
|
+
DYNAMODB_ENDPOINT: `http://127.0.0.1:${dynamodbPort}`,
|
|
712
|
+
DYNAMODB_TABLE_NAME: tableName,
|
|
713
|
+
};
|
|
714
|
+
const envFile = `# DynamoDB Local Configuration
|
|
715
|
+
DYNAMODB_TABLE_NAME=${tableName}
|
|
716
|
+
DYNAMODB_ENDPOINT=http://127.0.0.1:${dynamodbPort}
|
|
717
|
+
AWS_REGION=us-east-1
|
|
718
|
+
`;
|
|
719
|
+
return {
|
|
720
|
+
dockerCompose,
|
|
721
|
+
envFile,
|
|
722
|
+
envVars,
|
|
723
|
+
};
|
|
724
|
+
},
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
const DEFAULT_REGION = "us-east-1";
|
|
728
|
+
/**
|
|
729
|
+
* Ensure DynamoDB client is initialized from environment variables
|
|
730
|
+
* Called automatically before each MCP tool execution
|
|
731
|
+
*/
|
|
732
|
+
function ensureInitialized() {
|
|
733
|
+
if (isInitialized()) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const tableName = process.env.DYNAMODB_TABLE_NAME;
|
|
737
|
+
if (!tableName) {
|
|
738
|
+
throw new errors.ConfigurationError("DYNAMODB_TABLE_NAME environment variable is required");
|
|
739
|
+
}
|
|
740
|
+
initClient({
|
|
741
|
+
endpoint: process.env.DYNAMODB_ENDPOINT,
|
|
742
|
+
region: process.env.AWS_REGION || DEFAULT_REGION,
|
|
743
|
+
tableName,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Check DynamoDB connection status and configuration
|
|
749
|
+
*/
|
|
750
|
+
const statusHandler = vocabulary.serviceHandler({
|
|
751
|
+
alias: "dynamodb_status",
|
|
752
|
+
description: "Check DynamoDB connection status and configuration",
|
|
753
|
+
service: async () => {
|
|
754
|
+
ensureInitialized();
|
|
755
|
+
return {
|
|
756
|
+
endpoint: process.env.DYNAMODB_ENDPOINT || "AWS Default",
|
|
757
|
+
initialized: isInitialized(),
|
|
758
|
+
region: process.env.AWS_REGION || "us-east-1",
|
|
759
|
+
tableName: getTableName(),
|
|
760
|
+
};
|
|
761
|
+
},
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Wrap a handler to auto-initialize before execution
|
|
766
|
+
*/
|
|
767
|
+
function wrapWithInit(handler) {
|
|
768
|
+
const wrapped = async (input) => {
|
|
769
|
+
ensureInitialized();
|
|
770
|
+
return handler(input);
|
|
771
|
+
};
|
|
772
|
+
// Preserve handler properties for MCP registration
|
|
773
|
+
Object.assign(wrapped, {
|
|
774
|
+
alias: handler.alias,
|
|
775
|
+
description: handler.description,
|
|
776
|
+
input: handler.input,
|
|
777
|
+
});
|
|
778
|
+
return wrapped;
|
|
779
|
+
}
|
|
780
|
+
// MCP-specific serviceHandler wrappers for functions with complex inputs
|
|
781
|
+
// Note: These wrap the regular async functions to make them work with registerMcpTool
|
|
782
|
+
/**
|
|
783
|
+
* MCP wrapper for putEntity
|
|
784
|
+
* Accepts entity JSON directly from LLM
|
|
785
|
+
*/
|
|
786
|
+
const mcpPutEntity = vocabulary.serviceHandler({
|
|
787
|
+
alias: "dynamodb_put",
|
|
788
|
+
description: "Create or replace an entity in DynamoDB (auto-indexes GSI keys)",
|
|
789
|
+
input: {
|
|
790
|
+
// Required entity fields
|
|
791
|
+
id: { type: String, description: "Entity ID (sort key)" },
|
|
792
|
+
model: { type: String, description: "Entity model name (partition key)" },
|
|
793
|
+
name: { type: String, description: "Entity name" },
|
|
794
|
+
ou: { type: String, description: "Organizational unit (@ for root)" },
|
|
795
|
+
// Optional fields
|
|
796
|
+
alias: { type: String, required: false, description: "Human-friendly alias" },
|
|
797
|
+
class: { type: String, required: false, description: "Category classification" },
|
|
798
|
+
type: { type: String, required: false, description: "Type classification" },
|
|
799
|
+
xid: { type: String, required: false, description: "External ID" },
|
|
800
|
+
},
|
|
801
|
+
service: async (input) => {
|
|
802
|
+
const now = new Date().toISOString();
|
|
803
|
+
const entity = {
|
|
804
|
+
alias: input.alias,
|
|
805
|
+
class: input.class,
|
|
806
|
+
createdAt: now,
|
|
807
|
+
id: input.id,
|
|
808
|
+
model: input.model,
|
|
809
|
+
name: input.name,
|
|
810
|
+
ou: input.ou,
|
|
811
|
+
sequence: Date.now(),
|
|
812
|
+
type: input.type,
|
|
813
|
+
updatedAt: now,
|
|
814
|
+
xid: input.xid,
|
|
815
|
+
};
|
|
816
|
+
return putEntity({ entity });
|
|
817
|
+
},
|
|
818
|
+
});
|
|
819
|
+
/**
|
|
820
|
+
* MCP wrapper for updateEntity
|
|
821
|
+
* Accepts entity JSON directly from LLM
|
|
822
|
+
*/
|
|
823
|
+
const mcpUpdateEntity = vocabulary.serviceHandler({
|
|
824
|
+
alias: "dynamodb_update",
|
|
825
|
+
description: "Update an entity in DynamoDB (sets updatedAt, re-indexes GSI keys)",
|
|
826
|
+
input: {
|
|
827
|
+
// Required fields to identify the entity
|
|
828
|
+
id: { type: String, description: "Entity ID (sort key)" },
|
|
829
|
+
model: { type: String, description: "Entity model name (partition key)" },
|
|
830
|
+
// Fields that can be updated
|
|
831
|
+
name: { type: String, required: false, description: "Entity name" },
|
|
832
|
+
ou: { type: String, required: false, description: "Organizational unit" },
|
|
833
|
+
alias: { type: String, required: false, description: "Human-friendly alias" },
|
|
834
|
+
class: { type: String, required: false, description: "Category classification" },
|
|
835
|
+
type: { type: String, required: false, description: "Type classification" },
|
|
836
|
+
xid: { type: String, required: false, description: "External ID" },
|
|
837
|
+
},
|
|
838
|
+
service: async (input) => {
|
|
839
|
+
// First get the existing entity
|
|
840
|
+
const existing = await getEntity({
|
|
841
|
+
id: input.id,
|
|
842
|
+
model: input.model,
|
|
843
|
+
});
|
|
844
|
+
if (!existing) {
|
|
845
|
+
return { error: "Entity not found", id: input.id, model: input.model };
|
|
846
|
+
}
|
|
847
|
+
// Merge updates
|
|
848
|
+
const entity = {
|
|
849
|
+
...existing,
|
|
850
|
+
...(input.alias !== undefined && { alias: input.alias }),
|
|
851
|
+
...(input.class !== undefined && { class: input.class }),
|
|
852
|
+
...(input.name !== undefined && { name: input.name }),
|
|
853
|
+
...(input.ou !== undefined && { ou: input.ou }),
|
|
854
|
+
...(input.type !== undefined && { type: input.type }),
|
|
855
|
+
...(input.xid !== undefined && { xid: input.xid }),
|
|
856
|
+
};
|
|
857
|
+
return updateEntity({ entity });
|
|
858
|
+
},
|
|
859
|
+
});
|
|
860
|
+
/**
|
|
861
|
+
* MCP wrapper for queryByOu
|
|
862
|
+
* Note: Pagination via startKey is not exposed to MCP; use limit instead
|
|
863
|
+
*/
|
|
864
|
+
const mcpQueryByOu = vocabulary.serviceHandler({
|
|
865
|
+
alias: "dynamodb_query_ou",
|
|
866
|
+
description: "Query entities by organizational unit (parent hierarchy)",
|
|
867
|
+
input: {
|
|
868
|
+
model: { type: String, description: "Entity model name" },
|
|
869
|
+
ou: { type: String, description: "Organizational unit (@ for root)" },
|
|
870
|
+
archived: {
|
|
871
|
+
type: Boolean,
|
|
872
|
+
default: false,
|
|
873
|
+
required: false,
|
|
874
|
+
description: "Query archived entities instead of active ones",
|
|
875
|
+
},
|
|
876
|
+
ascending: {
|
|
877
|
+
type: Boolean,
|
|
878
|
+
default: false,
|
|
879
|
+
required: false,
|
|
880
|
+
description: "Sort ascending (oldest first)",
|
|
881
|
+
},
|
|
882
|
+
deleted: {
|
|
883
|
+
type: Boolean,
|
|
884
|
+
default: false,
|
|
885
|
+
required: false,
|
|
886
|
+
description: "Query deleted entities instead of active ones",
|
|
887
|
+
},
|
|
888
|
+
limit: {
|
|
889
|
+
type: Number,
|
|
890
|
+
required: false,
|
|
891
|
+
description: "Maximum number of items to return",
|
|
892
|
+
},
|
|
893
|
+
},
|
|
894
|
+
service: async (input) => {
|
|
895
|
+
return queryByOu({
|
|
896
|
+
archived: input.archived,
|
|
897
|
+
ascending: input.ascending,
|
|
898
|
+
deleted: input.deleted,
|
|
899
|
+
limit: input.limit,
|
|
900
|
+
model: input.model,
|
|
901
|
+
ou: input.ou,
|
|
902
|
+
});
|
|
903
|
+
},
|
|
904
|
+
});
|
|
905
|
+
/**
|
|
906
|
+
* MCP wrapper for queryByClass
|
|
907
|
+
* Note: Pagination via startKey is not exposed to MCP; use limit instead
|
|
908
|
+
*/
|
|
909
|
+
const mcpQueryByClass = vocabulary.serviceHandler({
|
|
910
|
+
alias: "dynamodb_query_class",
|
|
911
|
+
description: "Query entities by category classification",
|
|
912
|
+
input: {
|
|
913
|
+
model: { type: String, description: "Entity model name" },
|
|
914
|
+
ou: { type: String, description: "Organizational unit (@ for root)" },
|
|
915
|
+
recordClass: { type: String, description: "Category classification" },
|
|
916
|
+
archived: {
|
|
917
|
+
type: Boolean,
|
|
918
|
+
default: false,
|
|
919
|
+
required: false,
|
|
920
|
+
description: "Query archived entities instead of active ones",
|
|
921
|
+
},
|
|
922
|
+
ascending: {
|
|
923
|
+
type: Boolean,
|
|
924
|
+
default: false,
|
|
925
|
+
required: false,
|
|
926
|
+
description: "Sort ascending (oldest first)",
|
|
927
|
+
},
|
|
928
|
+
deleted: {
|
|
929
|
+
type: Boolean,
|
|
930
|
+
default: false,
|
|
931
|
+
required: false,
|
|
932
|
+
description: "Query deleted entities instead of active ones",
|
|
933
|
+
},
|
|
934
|
+
limit: {
|
|
935
|
+
type: Number,
|
|
936
|
+
required: false,
|
|
937
|
+
description: "Maximum number of items to return",
|
|
938
|
+
},
|
|
939
|
+
},
|
|
940
|
+
service: async (input) => {
|
|
941
|
+
return queryByClass({
|
|
942
|
+
archived: input.archived,
|
|
943
|
+
ascending: input.ascending,
|
|
944
|
+
deleted: input.deleted,
|
|
945
|
+
limit: input.limit,
|
|
946
|
+
model: input.model,
|
|
947
|
+
ou: input.ou,
|
|
948
|
+
recordClass: input.recordClass,
|
|
949
|
+
});
|
|
950
|
+
},
|
|
951
|
+
});
|
|
952
|
+
/**
|
|
953
|
+
* MCP wrapper for queryByType
|
|
954
|
+
* Note: Pagination via startKey is not exposed to MCP; use limit instead
|
|
955
|
+
*/
|
|
956
|
+
const mcpQueryByType = vocabulary.serviceHandler({
|
|
957
|
+
alias: "dynamodb_query_type",
|
|
958
|
+
description: "Query entities by type classification",
|
|
959
|
+
input: {
|
|
960
|
+
model: { type: String, description: "Entity model name" },
|
|
961
|
+
ou: { type: String, description: "Organizational unit (@ for root)" },
|
|
962
|
+
type: { type: String, description: "Type classification" },
|
|
963
|
+
archived: {
|
|
964
|
+
type: Boolean,
|
|
965
|
+
default: false,
|
|
966
|
+
required: false,
|
|
967
|
+
description: "Query archived entities instead of active ones",
|
|
968
|
+
},
|
|
969
|
+
ascending: {
|
|
970
|
+
type: Boolean,
|
|
971
|
+
default: false,
|
|
972
|
+
required: false,
|
|
973
|
+
description: "Sort ascending (oldest first)",
|
|
974
|
+
},
|
|
975
|
+
deleted: {
|
|
976
|
+
type: Boolean,
|
|
977
|
+
default: false,
|
|
978
|
+
required: false,
|
|
979
|
+
description: "Query deleted entities instead of active ones",
|
|
980
|
+
},
|
|
981
|
+
limit: {
|
|
982
|
+
type: Number,
|
|
983
|
+
required: false,
|
|
984
|
+
description: "Maximum number of items to return",
|
|
985
|
+
},
|
|
986
|
+
},
|
|
987
|
+
service: async (input) => {
|
|
988
|
+
return queryByType({
|
|
989
|
+
archived: input.archived,
|
|
990
|
+
ascending: input.ascending,
|
|
991
|
+
deleted: input.deleted,
|
|
992
|
+
limit: input.limit,
|
|
993
|
+
model: input.model,
|
|
994
|
+
ou: input.ou,
|
|
995
|
+
type: input.type,
|
|
996
|
+
});
|
|
997
|
+
},
|
|
998
|
+
});
|
|
999
|
+
/**
|
|
1000
|
+
* Register all DynamoDB MCP tools with a server
|
|
1001
|
+
*/
|
|
1002
|
+
function registerDynamoDbTools(config) {
|
|
1003
|
+
const { includeAdmin = true, server } = config;
|
|
1004
|
+
const tools = [];
|
|
1005
|
+
// Entity operations
|
|
1006
|
+
mcp.registerMcpTool({
|
|
1007
|
+
handler: wrapWithInit(getEntity),
|
|
1008
|
+
name: "dynamodb_get",
|
|
1009
|
+
server,
|
|
1010
|
+
});
|
|
1011
|
+
tools.push("dynamodb_get");
|
|
1012
|
+
mcp.registerMcpTool({
|
|
1013
|
+
handler: wrapWithInit(mcpPutEntity),
|
|
1014
|
+
name: "dynamodb_put",
|
|
1015
|
+
server,
|
|
1016
|
+
});
|
|
1017
|
+
tools.push("dynamodb_put");
|
|
1018
|
+
mcp.registerMcpTool({
|
|
1019
|
+
handler: wrapWithInit(mcpUpdateEntity),
|
|
1020
|
+
name: "dynamodb_update",
|
|
1021
|
+
server,
|
|
1022
|
+
});
|
|
1023
|
+
tools.push("dynamodb_update");
|
|
1024
|
+
mcp.registerMcpTool({
|
|
1025
|
+
handler: wrapWithInit(deleteEntity),
|
|
1026
|
+
name: "dynamodb_delete",
|
|
1027
|
+
server,
|
|
1028
|
+
});
|
|
1029
|
+
tools.push("dynamodb_delete");
|
|
1030
|
+
mcp.registerMcpTool({
|
|
1031
|
+
handler: wrapWithInit(archiveEntity),
|
|
1032
|
+
name: "dynamodb_archive",
|
|
1033
|
+
server,
|
|
1034
|
+
});
|
|
1035
|
+
tools.push("dynamodb_archive");
|
|
1036
|
+
mcp.registerMcpTool({
|
|
1037
|
+
handler: wrapWithInit(destroyEntity),
|
|
1038
|
+
name: "dynamodb_destroy",
|
|
1039
|
+
server,
|
|
1040
|
+
});
|
|
1041
|
+
tools.push("dynamodb_destroy");
|
|
1042
|
+
// Query operations
|
|
1043
|
+
mcp.registerMcpTool({
|
|
1044
|
+
handler: wrapWithInit(mcpQueryByOu),
|
|
1045
|
+
name: "dynamodb_query_ou",
|
|
1046
|
+
server,
|
|
1047
|
+
});
|
|
1048
|
+
tools.push("dynamodb_query_ou");
|
|
1049
|
+
mcp.registerMcpTool({
|
|
1050
|
+
handler: wrapWithInit(queryByAlias),
|
|
1051
|
+
name: "dynamodb_query_alias",
|
|
1052
|
+
server,
|
|
1053
|
+
});
|
|
1054
|
+
tools.push("dynamodb_query_alias");
|
|
1055
|
+
mcp.registerMcpTool({
|
|
1056
|
+
handler: wrapWithInit(mcpQueryByClass),
|
|
1057
|
+
name: "dynamodb_query_class",
|
|
1058
|
+
server,
|
|
1059
|
+
});
|
|
1060
|
+
tools.push("dynamodb_query_class");
|
|
1061
|
+
mcp.registerMcpTool({
|
|
1062
|
+
handler: wrapWithInit(mcpQueryByType),
|
|
1063
|
+
name: "dynamodb_query_type",
|
|
1064
|
+
server,
|
|
1065
|
+
});
|
|
1066
|
+
tools.push("dynamodb_query_type");
|
|
1067
|
+
mcp.registerMcpTool({
|
|
1068
|
+
handler: wrapWithInit(queryByXid),
|
|
1069
|
+
name: "dynamodb_query_xid",
|
|
1070
|
+
server,
|
|
1071
|
+
});
|
|
1072
|
+
tools.push("dynamodb_query_xid");
|
|
1073
|
+
// Admin tools (MCP-only)
|
|
1074
|
+
if (includeAdmin) {
|
|
1075
|
+
mcp.registerMcpTool({ handler: statusHandler, server });
|
|
1076
|
+
tools.push("dynamodb_status");
|
|
1077
|
+
mcp.registerMcpTool({ handler: createTableHandler, server });
|
|
1078
|
+
tools.push("dynamodb_create_table");
|
|
1079
|
+
mcp.registerMcpTool({ handler: dockerComposeHandler, server });
|
|
1080
|
+
tools.push("dynamodb_generate_docker_compose");
|
|
1081
|
+
}
|
|
1082
|
+
return { tools };
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
exports.createTableHandler = createTableHandler;
|
|
1086
|
+
exports.dockerComposeHandler = dockerComposeHandler;
|
|
1087
|
+
exports.ensureInitialized = ensureInitialized;
|
|
1088
|
+
exports.registerDynamoDbTools = registerDynamoDbTools;
|
|
1089
|
+
exports.statusHandler = statusHandler;
|
|
1090
|
+
//# sourceMappingURL=index.cjs.map
|