estatehelm 1.0.22 → 1.0.24

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/index.js CHANGED
@@ -611,12 +611,18 @@ function base64Decode(base64) {
611
611
  throw new Error(`Failed to decode base64: ${error instanceof Error ? error.message : "Unknown error"}`);
612
612
  }
613
613
  }
614
+ function generateRandomBytes(length) {
615
+ return crypto.getRandomValues(new Uint8Array(length));
616
+ }
614
617
  function stringToBytes(str) {
615
618
  return new TextEncoder().encode(str);
616
619
  }
617
620
  function bytesToString(bytes) {
618
621
  return new TextDecoder().decode(bytes);
619
622
  }
623
+ function generateUUID() {
624
+ return crypto.randomUUID();
625
+ }
620
626
 
621
627
  // ../encryption/src/householdKeys.ts
622
628
  var HOUSEHOLD_KEY_SIZE = 32;
@@ -697,6 +703,13 @@ async function importEntityKey(entityKeyBytes) {
697
703
  // ../encryption/src/entityEncryption.ts
698
704
  var IV_SIZE = 12;
699
705
  var AUTH_TAG_SIZE = 16;
706
+ function packEncryptedBlob(version, iv, ciphertext) {
707
+ const blob = new Uint8Array(1 + iv.length + ciphertext.length);
708
+ blob[0] = version;
709
+ blob.set(iv, 1);
710
+ blob.set(ciphertext, 1 + iv.length);
711
+ return base64Encode(blob);
712
+ }
700
713
  function unpackEncryptedBlob(packedBlob) {
701
714
  const blob = base64Decode(packedBlob);
702
715
  if (blob.length < 1 + IV_SIZE + AUTH_TAG_SIZE) {
@@ -707,6 +720,41 @@ function unpackEncryptedBlob(packedBlob) {
707
720
  const ciphertext = blob.slice(1 + IV_SIZE);
708
721
  return { version, iv, ciphertext };
709
722
  }
723
+ async function encryptEntity(householdKey, entityId, entityType, keyType, entityData, options = {}) {
724
+ try {
725
+ const entityKeyBytes = await deriveEntityKey(householdKey, entityId, entityType);
726
+ const entityKey = await importEntityKey(entityKeyBytes);
727
+ const jsonData = JSON.stringify(entityData);
728
+ const dataBytes = stringToBytes(jsonData);
729
+ const iv = generateRandomBytes(IV_SIZE);
730
+ const encryptParams = {
731
+ name: "AES-GCM",
732
+ iv
733
+ };
734
+ if (options.additionalData) {
735
+ encryptParams.additionalData = options.additionalData;
736
+ }
737
+ const ciphertext = await crypto.subtle.encrypt(
738
+ encryptParams,
739
+ entityKey,
740
+ dataBytes
741
+ );
742
+ const result = {
743
+ entityId,
744
+ entityType,
745
+ keyType,
746
+ ciphertext: base64Encode(new Uint8Array(ciphertext)),
747
+ iv: base64Encode(iv),
748
+ encryptedAt: /* @__PURE__ */ new Date(),
749
+ derivedEntityKey: entityKeyBytes
750
+ };
751
+ return result;
752
+ } catch (error) {
753
+ throw new Error(
754
+ `Failed to encrypt ${entityType} entity ${entityId}: ${error instanceof Error ? error.message : "Unknown error"}`
755
+ );
756
+ }
757
+ }
710
758
  async function decryptEntity(householdKey, encrypted, options = {}) {
711
759
  if (options.expectedType && encrypted.entityType !== options.expectedType) {
712
760
  throw new Error(
@@ -826,6 +874,12 @@ function getKeyTypeForEntity(entityType) {
826
874
  }
827
875
  return config.keyType;
828
876
  }
877
+ function resolveEntityAlias(entityType) {
878
+ if (entityType in ENTITY_ALIASES) {
879
+ return ENTITY_ALIASES[entityType];
880
+ }
881
+ return entityType;
882
+ }
829
883
 
830
884
  // ../types/src/options.ts
831
885
  var PERSONAL_LEGAL_DOCUMENT_TYPE_OPTIONS = [
@@ -1101,6 +1155,7 @@ var ENTITY_KEY_TYPE_MAP = {
1101
1155
  Object.entries(ENTITY_ALIASES).map(([alias, target]) => [alias, ENTITIES[target].keyType])
1102
1156
  )
1103
1157
  };
1158
+ var CURRENT_CRYPTO_VERSION = 1;
1104
1159
 
1105
1160
  // ../encryption/src/recoveryKey.ts
1106
1161
  var RECOVERY_KEY_SIZE = 16;
@@ -1315,6 +1370,38 @@ var RP_ID = typeof window !== "undefined" ? window.location.hostname : "estatehe
1315
1370
 
1316
1371
  // ../encryption/src/fileEncryption.ts
1317
1372
  var FILE_IV_SIZE = 12;
1373
+ async function encryptFileToBytes(householdKey, entityId, entityType, fileBytes, mimeType, options = {}) {
1374
+ const keyDerivationId = options.fileId || entityId;
1375
+ try {
1376
+ const fileKeyBytes = await deriveEntityKey(householdKey, keyDerivationId, entityType);
1377
+ const fileKey = await importEntityKey(fileKeyBytes);
1378
+ const iv = generateRandomBytes(FILE_IV_SIZE);
1379
+ const encryptParams = {
1380
+ name: "AES-GCM",
1381
+ iv
1382
+ };
1383
+ if (options.additionalData) {
1384
+ encryptParams.additionalData = options.additionalData;
1385
+ }
1386
+ const ciphertext = await crypto.subtle.encrypt(
1387
+ encryptParams,
1388
+ fileKey,
1389
+ fileBytes
1390
+ );
1391
+ const packed = new Uint8Array(FILE_IV_SIZE + ciphertext.byteLength);
1392
+ packed.set(iv, 0);
1393
+ packed.set(new Uint8Array(ciphertext), FILE_IV_SIZE);
1394
+ return {
1395
+ encryptedBytes: packed,
1396
+ mimeType,
1397
+ originalSize: fileBytes.length
1398
+ };
1399
+ } catch (error) {
1400
+ throw new Error(
1401
+ `Failed to encrypt file for ${entityType} ${keyDerivationId}: ${error instanceof Error ? error.message : "Unknown error"}`
1402
+ );
1403
+ }
1404
+ }
1318
1405
  async function decryptFileFromArrayBuffer(householdKey, entityId, entityType, encryptedArrayBuffer, mimeType) {
1319
1406
  const packed = new Uint8Array(encryptedArrayBuffer);
1320
1407
  if (packed.length < FILE_IV_SIZE + 16) {
@@ -1609,7 +1696,7 @@ function trackLogout() {
1609
1696
  trackEvent("mcp_logout");
1610
1697
  }
1611
1698
  function trackToolUse(tool) {
1612
- trackEvent("mcp_tool_use", { tool });
1699
+ trackEvent(`mcp_${tool}`);
1613
1700
  }
1614
1701
  function trackSearch(resultCount, hasEntityType) {
1615
1702
  trackEvent("mcp_search", { resultCount, filtered: hasEntityType });
@@ -1895,7 +1982,7 @@ async function getPrivateKey(mcpToken) {
1895
1982
  // src/server.ts
1896
1983
  var import_server = require("@modelcontextprotocol/sdk/server/index.js");
1897
1984
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
1898
- var import_types44 = require("@modelcontextprotocol/sdk/types.js");
1985
+ var import_types46 = require("@modelcontextprotocol/sdk/types.js");
1899
1986
 
1900
1987
  // ../cache-sqlite/src/sqliteStore.ts
1901
1988
  var import_better_sqlite3 = __toESM(require("better-sqlite3"));
@@ -2507,6 +2594,10 @@ async function loadHouseholdKeys(client, householdId, privateKey) {
2507
2594
  function getHouseholdKey(householdId, keyType) {
2508
2595
  return householdKeysCache.get(`${householdId}:${keyType}`);
2509
2596
  }
2597
+ function getHouseholdKeyForEntity(householdId, entityType) {
2598
+ const keyType = getKeyTypeForEntity(entityType);
2599
+ return getHouseholdKey(householdId, keyType);
2600
+ }
2510
2601
  async function getDecryptedEntities(householdId, entityType, privateKey) {
2511
2602
  const cache = getCache();
2512
2603
  const cacheEntry = await cache.getEntityCache(householdId, entityType);
@@ -3503,6 +3594,85 @@ function indexAllEntities(data) {
3503
3594
  return entities;
3504
3595
  }
3505
3596
 
3597
+ // ../utils/src/entityMerge.ts
3598
+ var ENTITY_METADATA_FIELDS = [
3599
+ "id",
3600
+ "created_at",
3601
+ "updated_at",
3602
+ "created_by",
3603
+ "household_id",
3604
+ "owner_user_id",
3605
+ "version",
3606
+ "key_type",
3607
+ "entity_type",
3608
+ "entityType",
3609
+ "householdId",
3610
+ "ownerUserId",
3611
+ "createdAt",
3612
+ "updatedAt",
3613
+ "createdBy",
3614
+ "keyType"
3615
+ ];
3616
+ function deepMergeEntity(existing, partial) {
3617
+ const result = { ...existing };
3618
+ for (const key of Object.keys(partial)) {
3619
+ const newValue = partial[key];
3620
+ if (newValue === void 0) {
3621
+ continue;
3622
+ }
3623
+ if (newValue === null) {
3624
+ result[key] = null;
3625
+ continue;
3626
+ }
3627
+ if (Array.isArray(newValue)) {
3628
+ result[key] = [...newValue];
3629
+ continue;
3630
+ }
3631
+ if (typeof newValue === "object" && newValue !== null && typeof result[key] === "object" && result[key] !== null && !Array.isArray(result[key])) {
3632
+ result[key] = deepMergeEntity(
3633
+ result[key],
3634
+ newValue
3635
+ );
3636
+ continue;
3637
+ }
3638
+ result[key] = newValue;
3639
+ }
3640
+ return result;
3641
+ }
3642
+ function appendToEntityArrays(existing, appendToArrays) {
3643
+ const result = { ...existing };
3644
+ for (const [fieldName, itemsToAppend] of Object.entries(appendToArrays)) {
3645
+ if (!Array.isArray(itemsToAppend) || itemsToAppend.length === 0) {
3646
+ continue;
3647
+ }
3648
+ const existingArray = result[fieldName];
3649
+ if (Array.isArray(existingArray)) {
3650
+ ;
3651
+ result[fieldName] = [...existingArray, ...itemsToAppend];
3652
+ } else {
3653
+ ;
3654
+ result[fieldName] = [...itemsToAppend];
3655
+ }
3656
+ }
3657
+ return result;
3658
+ }
3659
+ function normalizeEntityData(data) {
3660
+ const normalized = { ...data };
3661
+ if ("entity_type" in normalized && normalized.entity_type !== void 0 && !("person_type" in normalized && normalized.person_type !== void 0)) {
3662
+ ;
3663
+ normalized.person_type = normalized.entity_type;
3664
+ }
3665
+ delete normalized.entity_type;
3666
+ return normalized;
3667
+ }
3668
+ function stripEntityMetadata(entity) {
3669
+ const result = { ...entity };
3670
+ for (const field of ENTITY_METADATA_FIELDS) {
3671
+ delete result[field];
3672
+ }
3673
+ return result;
3674
+ }
3675
+
3506
3676
  // ../list-data/src/adapters/contactAdapter.ts
3507
3677
  function getContactComputedFields(contact) {
3508
3678
  const computed = {
@@ -5061,6 +5231,388 @@ async function executeFileDownload(client, params) {
5061
5231
  }
5062
5232
  }
5063
5233
 
5234
+ // src/tools/entities.ts
5235
+ var import_fs = require("fs");
5236
+ var import_path = require("path");
5237
+ var import_ajv = __toESM(require("ajv"));
5238
+ var ajv = new import_ajv.default({ allErrors: true, strict: false });
5239
+ var schemaValidators = /* @__PURE__ */ new Map();
5240
+ function makeSchemaStrict(schema) {
5241
+ const result = { ...schema };
5242
+ if (result.properties && result.additionalProperties === void 0) {
5243
+ result.additionalProperties = false;
5244
+ }
5245
+ if (result.definitions && typeof result.definitions === "object") {
5246
+ const strictDefinitions = {};
5247
+ for (const [name, def] of Object.entries(result.definitions)) {
5248
+ if (def && typeof def === "object" && "properties" in def) {
5249
+ strictDefinitions[name] = {
5250
+ ...def,
5251
+ additionalProperties: def.additionalProperties ?? false
5252
+ };
5253
+ } else {
5254
+ strictDefinitions[name] = def;
5255
+ }
5256
+ }
5257
+ result.definitions = strictDefinitions;
5258
+ }
5259
+ return result;
5260
+ }
5261
+ function findUnknownFields(data, schema) {
5262
+ const validProperties = schema.properties;
5263
+ if (!validProperties) {
5264
+ return [];
5265
+ }
5266
+ const validKeys = new Set(Object.keys(validProperties));
5267
+ const unknownFields = [];
5268
+ for (const key of Object.keys(data)) {
5269
+ if (!validKeys.has(key)) {
5270
+ unknownFields.push(key);
5271
+ }
5272
+ }
5273
+ return unknownFields;
5274
+ }
5275
+ var schemaCache = /* @__PURE__ */ new Map();
5276
+ function getSchemaDir() {
5277
+ try {
5278
+ const typesPath = require.resolve("@hearthcoo/types");
5279
+ const typesDir = (0, import_path.dirname)((0, import_path.dirname)(typesPath));
5280
+ return (0, import_path.join)(typesDir, "dist", "schemas");
5281
+ } catch {
5282
+ return (0, import_path.join)(process.cwd(), "..", "types", "dist", "schemas");
5283
+ }
5284
+ }
5285
+ async function getSchemaValidator(entityType) {
5286
+ if (schemaValidators.has(entityType)) {
5287
+ return schemaValidators.get(entityType);
5288
+ }
5289
+ if (!SCHEMA_ENTITY_TYPES.includes(entityType)) {
5290
+ console.error(`[Entities] No schema for ${entityType}, skipping validation`);
5291
+ return null;
5292
+ }
5293
+ try {
5294
+ const schemaDir = getSchemaDir();
5295
+ const schemaPath = (0, import_path.join)(schemaDir, `${entityType}.json`);
5296
+ console.error(`[Entities] Loading schema from: ${schemaPath}`);
5297
+ const schemaContent = (0, import_fs.readFileSync)(schemaPath, "utf-8");
5298
+ const schema = JSON.parse(schemaContent);
5299
+ console.error(`[Entities] Schema loaded, properties:`, Object.keys(schema.properties || {}));
5300
+ const strictSchema = makeSchemaStrict(schema);
5301
+ console.error(`[Entities] Strict schema additionalProperties:`, strictSchema.additionalProperties);
5302
+ const validator = ajv.compile(strictSchema);
5303
+ schemaValidators.set(entityType, validator);
5304
+ return validator;
5305
+ } catch (err) {
5306
+ console.error(`[Entities] Failed to load schema for ${entityType}:`, err);
5307
+ return null;
5308
+ }
5309
+ }
5310
+ async function getEntitySchema(entityTypeOrAlias) {
5311
+ const entityType = resolveEntityAlias(entityTypeOrAlias);
5312
+ if (!SCHEMA_ENTITY_TYPES.includes(entityType)) {
5313
+ return {
5314
+ success: false,
5315
+ error: `No schema available for entity type: ${entityType}. Valid types with schemas: ${SCHEMA_ENTITY_TYPES.join(", ")}`
5316
+ };
5317
+ }
5318
+ try {
5319
+ const schemaDir = getSchemaDir();
5320
+ const schemaPath = (0, import_path.join)(schemaDir, `${entityType}.json`);
5321
+ const schemaContent = (0, import_fs.readFileSync)(schemaPath, "utf-8");
5322
+ const schema = JSON.parse(schemaContent);
5323
+ return {
5324
+ success: true,
5325
+ entityType,
5326
+ schema: {
5327
+ properties: schema.properties || {},
5328
+ required: schema.required || [],
5329
+ definitions: schema.definitions || {}
5330
+ }
5331
+ };
5332
+ } catch (err) {
5333
+ return {
5334
+ success: false,
5335
+ error: `Failed to load schema for ${entityType}: ${err instanceof Error ? err.message : "Unknown error"}`
5336
+ };
5337
+ }
5338
+ }
5339
+ async function executeCreateEntity(client, privateKey, params) {
5340
+ const { householdId, data, entityId: providedId } = params;
5341
+ const entityType = resolveEntityAlias(params.entityType);
5342
+ try {
5343
+ const householdKey = getHouseholdKeyForEntity(householdId, entityType);
5344
+ if (!householdKey) {
5345
+ return {
5346
+ success: false,
5347
+ error: `No encryption key available for ${entityType}. Ensure cache is synced.`
5348
+ };
5349
+ }
5350
+ const validator = await getSchemaValidator(entityType);
5351
+ if (validator && !validator(data)) {
5352
+ const errors = ajv.errorsText(validator.errors);
5353
+ return {
5354
+ success: false,
5355
+ error: `Validation failed: ${errors}`
5356
+ };
5357
+ }
5358
+ const entityId = providedId || generateUUID();
5359
+ const normalizedData = normalizeEntityData(stripEntityMetadata(data));
5360
+ const keyType = getKeyTypeForEntity(entityType);
5361
+ const encrypted = await encryptEntity(
5362
+ householdKey,
5363
+ entityId,
5364
+ entityType,
5365
+ keyType,
5366
+ normalizedData
5367
+ );
5368
+ const encryptedBlob = packEncryptedBlob(
5369
+ CURRENT_CRYPTO_VERSION,
5370
+ base64Decode(encrypted.iv),
5371
+ base64Decode(encrypted.ciphertext)
5372
+ );
5373
+ const response = await client.createEntity(householdId, {
5374
+ id: entityId,
5375
+ entityType,
5376
+ keyType,
5377
+ encryptedData: encryptedBlob,
5378
+ cryptoVersion: CURRENT_CRYPTO_VERSION
5379
+ });
5380
+ await syncIfNeeded(client, privateKey, true);
5381
+ return {
5382
+ success: true,
5383
+ entityId: response.id,
5384
+ version: response.version
5385
+ };
5386
+ } catch (err) {
5387
+ if (err.status === 409) {
5388
+ return {
5389
+ success: false,
5390
+ error: "Entity with this ID already exists"
5391
+ };
5392
+ }
5393
+ return {
5394
+ success: false,
5395
+ error: err.message || "Failed to create entity"
5396
+ };
5397
+ }
5398
+ }
5399
+ async function executeUpdateEntity(client, privateKey, params) {
5400
+ const { householdId, entityId, data, appendToArrays } = params;
5401
+ const entityType = resolveEntityAlias(params.entityType);
5402
+ try {
5403
+ const householdKey = getHouseholdKeyForEntity(householdId, entityType);
5404
+ if (!householdKey) {
5405
+ return {
5406
+ success: false,
5407
+ error: `No encryption key available for ${entityType}. Ensure cache is synced.`
5408
+ };
5409
+ }
5410
+ let existingEntity;
5411
+ try {
5412
+ existingEntity = await client.getEntity(householdId, entityId);
5413
+ } catch (err) {
5414
+ if (err.status === 404) {
5415
+ return {
5416
+ success: false,
5417
+ error: `Entity not found: ${entityId}`
5418
+ };
5419
+ }
5420
+ throw err;
5421
+ }
5422
+ const { iv, ciphertext } = unpackEncryptedBlob(existingEntity.encryptedData);
5423
+ const encryptedForDecrypt = {
5424
+ entityId: existingEntity.id,
5425
+ entityType: existingEntity.entityType,
5426
+ keyType: existingEntity.keyType,
5427
+ ciphertext: base64Encode(ciphertext),
5428
+ iv: base64Encode(iv),
5429
+ encryptedAt: new Date(existingEntity.createdAt),
5430
+ derivedEntityKey: new Uint8Array()
5431
+ };
5432
+ const existingData = await decryptEntity(
5433
+ householdKey,
5434
+ encryptedForDecrypt
5435
+ );
5436
+ const cleanExisting = stripEntityMetadata(existingData);
5437
+ const strippedPartial = stripEntityMetadata(data);
5438
+ let mergedData = deepMergeEntity(cleanExisting, strippedPartial);
5439
+ if (appendToArrays && Object.keys(appendToArrays).length > 0) {
5440
+ mergedData = appendToEntityArrays(mergedData, appendToArrays);
5441
+ }
5442
+ const normalizedData = normalizeEntityData(mergedData);
5443
+ if (SCHEMA_ENTITY_TYPES.includes(entityType)) {
5444
+ try {
5445
+ if (!schemaCache.has(entityType)) {
5446
+ const schemaDir = getSchemaDir();
5447
+ const schemaPath = (0, import_path.join)(schemaDir, `${entityType}.json`);
5448
+ const schemaContent = (0, import_fs.readFileSync)(schemaPath, "utf-8");
5449
+ schemaCache.set(entityType, JSON.parse(schemaContent));
5450
+ }
5451
+ const schema = schemaCache.get(entityType);
5452
+ const newFieldsToValidate = stripEntityMetadata(data);
5453
+ const unknownFields = findUnknownFields(newFieldsToValidate, schema);
5454
+ if (unknownFields.length > 0) {
5455
+ const validFields = Object.keys(schema.properties || {});
5456
+ return {
5457
+ success: false,
5458
+ error: `Unknown fields: ${unknownFields.join(", ")}. Valid fields are: ${validFields.join(", ")}`
5459
+ };
5460
+ }
5461
+ } catch (err) {
5462
+ console.error(`[Entities] Failed to validate new fields:`, err);
5463
+ }
5464
+ }
5465
+ const keyType = getKeyTypeForEntity(entityType);
5466
+ const encrypted = await encryptEntity(
5467
+ householdKey,
5468
+ entityId,
5469
+ entityType,
5470
+ keyType,
5471
+ normalizedData
5472
+ );
5473
+ const encryptedBlob = packEncryptedBlob(
5474
+ CURRENT_CRYPTO_VERSION,
5475
+ base64Decode(encrypted.iv),
5476
+ base64Decode(encrypted.ciphertext)
5477
+ );
5478
+ try {
5479
+ const response = await client.updateEntity(
5480
+ householdId,
5481
+ entityId,
5482
+ {
5483
+ keyType,
5484
+ encryptedData: encryptedBlob,
5485
+ cryptoVersion: CURRENT_CRYPTO_VERSION
5486
+ },
5487
+ existingEntity.version
5488
+ );
5489
+ await syncIfNeeded(client, privateKey, true);
5490
+ return {
5491
+ success: true,
5492
+ version: response.version
5493
+ };
5494
+ } catch (err) {
5495
+ if (err.status === 412 || err.code === "VERSION_CONFLICT") {
5496
+ return {
5497
+ success: false,
5498
+ error: "VERSION_CONFLICT: Entity was modified by another user. Please retry."
5499
+ };
5500
+ }
5501
+ throw err;
5502
+ }
5503
+ } catch (err) {
5504
+ return {
5505
+ success: false,
5506
+ error: err.message || "Failed to update entity"
5507
+ };
5508
+ }
5509
+ }
5510
+
5511
+ // src/tools/upload.ts
5512
+ var import_promises = require("fs/promises");
5513
+ var import_path2 = require("path");
5514
+ var CRYPTO_VERSION = 2;
5515
+ async function executeFileUpload(client, params) {
5516
+ const { householdId, entityId, filePath, mimeType } = params;
5517
+ const entityType = resolveEntityAlias(params.entityType);
5518
+ try {
5519
+ const householdKey = getHouseholdKeyForEntity(householdId, entityType);
5520
+ if (!householdKey) {
5521
+ return {
5522
+ success: false,
5523
+ error: `No encryption key available for ${entityType}. Ensure cache is synced.`
5524
+ };
5525
+ }
5526
+ let fileBytes;
5527
+ let fileName;
5528
+ try {
5529
+ const buffer = await (0, import_promises.readFile)(filePath);
5530
+ fileBytes = new Uint8Array(buffer);
5531
+ fileName = (0, import_path2.basename)(filePath);
5532
+ } catch (err) {
5533
+ return {
5534
+ success: false,
5535
+ error: `Failed to read file: ${err.message}`
5536
+ };
5537
+ }
5538
+ const fileSizeBytes = fileBytes.length;
5539
+ const keyType = getKeyTypeForEntity(entityType);
5540
+ const uploadUrlResponse = await client.post(
5541
+ `/households/${householdId}/files/upload-url`,
5542
+ {
5543
+ contentType: mimeType,
5544
+ keyType,
5545
+ fileSizeBytes,
5546
+ includeThumbnailUrl: false
5547
+ // MCP doesn't generate thumbnails
5548
+ }
5549
+ );
5550
+ const fileId = uploadUrlResponse.fileId;
5551
+ const encrypted = await encryptFileToBytes(
5552
+ householdKey,
5553
+ entityId,
5554
+ entityType,
5555
+ fileBytes,
5556
+ mimeType,
5557
+ { fileId }
5558
+ );
5559
+ const uploadResponse = await fetch(uploadUrlResponse.url, {
5560
+ method: "PUT",
5561
+ headers: {
5562
+ "Content-Type": "application/octet-stream"
5563
+ },
5564
+ body: encrypted.encryptedBytes
5565
+ });
5566
+ if (!uploadResponse.ok) {
5567
+ return {
5568
+ success: false,
5569
+ error: `Upload to storage failed: ${uploadResponse.status}`
5570
+ };
5571
+ }
5572
+ const ext = fileName.includes(".") ? fileName.split(".").pop() : "";
5573
+ const genericName = ext ? `file.${ext}` : "file";
5574
+ const metadataResponse = await client.post(
5575
+ `/households/${householdId}/files`,
5576
+ {
5577
+ fileId,
5578
+ storagePath: uploadUrlResponse.storagePath,
5579
+ entityId,
5580
+ entityType,
5581
+ fileName: genericName,
5582
+ // Real name is in encrypted entity
5583
+ fileType: mimeType,
5584
+ fileSizeBytes,
5585
+ keyType,
5586
+ cryptoVersion: CRYPTO_VERSION
5587
+ }
5588
+ );
5589
+ return {
5590
+ success: true,
5591
+ fileId: metadataResponse.id,
5592
+ fileName,
5593
+ mimeType,
5594
+ size: fileSizeBytes
5595
+ };
5596
+ } catch (err) {
5597
+ if (err.status === 402) {
5598
+ return {
5599
+ success: false,
5600
+ error: "File uploads require an active subscription"
5601
+ };
5602
+ }
5603
+ if (err.status === 400 && err.error?.error === "attachment_error") {
5604
+ return {
5605
+ success: false,
5606
+ error: err.error?.message || "File type or size not allowed"
5607
+ };
5608
+ }
5609
+ return {
5610
+ success: false,
5611
+ error: err.message || "Failed to upload file"
5612
+ };
5613
+ }
5614
+ }
5615
+
5064
5616
  // src/server.ts
5065
5617
  async function startServer(mode, mcpToken) {
5066
5618
  const config = loadConfig();
@@ -5102,7 +5654,7 @@ async function startServer(mode, mcpToken) {
5102
5654
  }
5103
5655
  }
5104
5656
  );
5105
- server.setRequestHandler(import_types44.ListResourcesRequestSchema, async () => {
5657
+ server.setRequestHandler(import_types46.ListResourcesRequestSchema, async () => {
5106
5658
  const households = await listHouseholds();
5107
5659
  const resources = [
5108
5660
  {
@@ -5130,7 +5682,7 @@ async function startServer(mode, mcpToken) {
5130
5682
  }
5131
5683
  return { resources };
5132
5684
  });
5133
- server.setRequestHandler(import_types44.ReadResourceRequestSchema, async (request) => {
5685
+ server.setRequestHandler(import_types46.ReadResourceRequestSchema, async (request) => {
5134
5686
  const uri = request.params.uri;
5135
5687
  const parsed = parseResourceUri(uri);
5136
5688
  if (!parsed) {
@@ -5159,7 +5711,7 @@ async function startServer(mode, mcpToken) {
5159
5711
  ]
5160
5712
  };
5161
5713
  });
5162
- server.setRequestHandler(import_types44.ListToolsRequestSchema, async () => {
5714
+ server.setRequestHandler(import_types46.ListToolsRequestSchema, async () => {
5163
5715
  return {
5164
5716
  tools: [
5165
5717
  {
@@ -5248,11 +5800,111 @@ async function startServer(mode, mcpToken) {
5248
5800
  type: "object",
5249
5801
  properties: {}
5250
5802
  }
5803
+ },
5804
+ {
5805
+ name: "get_entity_schema",
5806
+ description: "Get the JSON schema for an entity type. Use this BEFORE create_entity or update_entity to discover valid field names and types.",
5807
+ inputSchema: {
5808
+ type: "object",
5809
+ properties: {
5810
+ entityType: {
5811
+ type: "string",
5812
+ description: "Entity type (e.g., pet, vehicle, insurance_policy, contact)"
5813
+ }
5814
+ },
5815
+ required: ["entityType"]
5816
+ }
5817
+ },
5818
+ {
5819
+ name: "create_entity",
5820
+ description: "Create a new encrypted entity. IMPORTANT: Call get_entity_schema first to discover valid field names - unknown fields will be rejected.",
5821
+ inputSchema: {
5822
+ type: "object",
5823
+ properties: {
5824
+ householdId: {
5825
+ type: "string",
5826
+ description: "The household ID to create the entity in"
5827
+ },
5828
+ entityType: {
5829
+ type: "string",
5830
+ description: "Entity type (e.g., pet, vehicle, insurance_policy, contact)"
5831
+ },
5832
+ data: {
5833
+ type: "object",
5834
+ description: "Entity data fields - must match schema from get_entity_schema"
5835
+ },
5836
+ entityId: {
5837
+ type: "string",
5838
+ description: "Optional: Pre-generated entity ID"
5839
+ }
5840
+ },
5841
+ required: ["householdId", "entityType", "data"]
5842
+ }
5843
+ },
5844
+ {
5845
+ name: "update_entity",
5846
+ description: "Update an existing entity with partial data. IMPORTANT: Call get_entity_schema first to discover valid field names - unknown fields will be rejected. Only fields provided will be changed; all other fields are preserved.",
5847
+ inputSchema: {
5848
+ type: "object",
5849
+ properties: {
5850
+ householdId: {
5851
+ type: "string",
5852
+ description: "The household ID"
5853
+ },
5854
+ entityType: {
5855
+ type: "string",
5856
+ description: "Entity type (e.g., pet, vehicle, insurance_policy)"
5857
+ },
5858
+ entityId: {
5859
+ type: "string",
5860
+ description: "The entity ID to update"
5861
+ },
5862
+ data: {
5863
+ type: "object",
5864
+ description: "Partial entity data - must match schema from get_entity_schema"
5865
+ },
5866
+ appendToArrays: {
5867
+ type: "object",
5868
+ description: 'Optional: Object mapping array field names to items to append (e.g., { photo_history: [{ file_id: "...", date: "..." }] })'
5869
+ }
5870
+ },
5871
+ required: ["householdId", "entityType", "entityId", "data"]
5872
+ }
5873
+ },
5874
+ {
5875
+ name: "upload_file",
5876
+ description: "Upload and encrypt a file attachment from disk. Returns a file ID that can be linked to an entity via update_entity with appendToArrays.",
5877
+ inputSchema: {
5878
+ type: "object",
5879
+ properties: {
5880
+ householdId: {
5881
+ type: "string",
5882
+ description: "The household ID"
5883
+ },
5884
+ entityId: {
5885
+ type: "string",
5886
+ description: "The entity ID to attach the file to"
5887
+ },
5888
+ entityType: {
5889
+ type: "string",
5890
+ description: "The entity type (e.g., pet, vehicle, insurance)"
5891
+ },
5892
+ filePath: {
5893
+ type: "string",
5894
+ description: "Absolute path to the file on disk"
5895
+ },
5896
+ mimeType: {
5897
+ type: "string",
5898
+ description: 'MIME type (e.g., "image/jpeg", "application/pdf")'
5899
+ }
5900
+ },
5901
+ required: ["householdId", "entityId", "entityType", "filePath", "mimeType"]
5902
+ }
5251
5903
  }
5252
5904
  ]
5253
5905
  };
5254
5906
  });
5255
- server.setRequestHandler(import_types44.CallToolRequestSchema, async (request) => {
5907
+ server.setRequestHandler(import_types46.CallToolRequestSchema, async (request) => {
5256
5908
  const { name, arguments: args } = request.params;
5257
5909
  trackToolUse(name);
5258
5910
  switch (name) {
@@ -5298,11 +5950,56 @@ async function startServer(mode, mcpToken) {
5298
5950
  }]
5299
5951
  };
5300
5952
  }
5953
+ case "get_entity_schema": {
5954
+ const { entityType } = args;
5955
+ const result = await getEntitySchema(entityType);
5956
+ return {
5957
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
5958
+ };
5959
+ }
5960
+ case "create_entity": {
5961
+ const createArgs = args;
5962
+ const result = await executeCreateEntity(client, privateKey, {
5963
+ householdId: createArgs.householdId,
5964
+ entityType: createArgs.entityType,
5965
+ data: createArgs.data,
5966
+ entityId: createArgs.entityId
5967
+ });
5968
+ return {
5969
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
5970
+ };
5971
+ }
5972
+ case "update_entity": {
5973
+ const updateArgs = args;
5974
+ const result = await executeUpdateEntity(client, privateKey, {
5975
+ householdId: updateArgs.householdId,
5976
+ entityType: updateArgs.entityType,
5977
+ entityId: updateArgs.entityId,
5978
+ data: updateArgs.data,
5979
+ appendToArrays: updateArgs.appendToArrays
5980
+ });
5981
+ return {
5982
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
5983
+ };
5984
+ }
5985
+ case "upload_file": {
5986
+ const uploadArgs = args;
5987
+ const result = await executeFileUpload(client, {
5988
+ householdId: uploadArgs.householdId,
5989
+ entityId: uploadArgs.entityId,
5990
+ entityType: uploadArgs.entityType,
5991
+ filePath: uploadArgs.filePath,
5992
+ mimeType: uploadArgs.mimeType
5993
+ });
5994
+ return {
5995
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
5996
+ };
5997
+ }
5301
5998
  default:
5302
5999
  throw new Error(`Unknown tool: ${name}`);
5303
6000
  }
5304
6001
  });
5305
- server.setRequestHandler(import_types44.ListPromptsRequestSchema, async () => {
6002
+ server.setRequestHandler(import_types46.ListPromptsRequestSchema, async () => {
5306
6003
  return {
5307
6004
  prompts: [
5308
6005
  {
@@ -5335,7 +6032,7 @@ async function startServer(mode, mcpToken) {
5335
6032
  ]
5336
6033
  };
5337
6034
  });
5338
- server.setRequestHandler(import_types44.GetPromptRequestSchema, async (request) => {
6035
+ server.setRequestHandler(import_types46.GetPromptRequestSchema, async (request) => {
5339
6036
  const { name, arguments: args } = request.params;
5340
6037
  switch (name) {
5341
6038
  case "household_summary": {