@vectros-ai/mcp-server 0.12.0 → 0.13.0

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
@@ -2471,7 +2471,7 @@ var AuthClient = class {
2471
2471
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/admin/access-log");
2472
2472
  }
2473
2473
  /**
2474
- * Lists all of your scoped API keys (`ssk_*`) across both your live and test environments. Revoked keys are excluded. Requires the `keys:r` scope.
2474
+ * Lists your scoped API keys (`ssk_*`) in your credential's own environment — a live key lists live keys, a test key lists test keys. Revoked keys are excluded. Requires the `keys:r` scope.
2475
2475
  *
2476
2476
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
2477
2477
  *
@@ -2854,7 +2854,7 @@ var AuthClient = class {
2854
2854
  );
2855
2855
  }
2856
2856
  /**
2857
- * Creates a new access profile under the given app context. This call is idempotent by `principalId`: if a profile with the same `principalId` already exists, the existing profile is returned (with status 200) instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing profile was returned) tells the two apart. To overwrite an existing profile's `scopes`/`roleId`, `identityOverrides`, and `status` instead of returning it unchanged, set `?upsert=true` (this also requires the `profiles:u` scope). You must provide exactly one of `scopes` (an inline list of scopes) or `roleId` (a reference to a role); supplying both, or neither, is rejected. `identityOverrides` is keyed by ownership namespace in `scope:<namespace>` form — `scope:org` and `scope:client` for the reserved namespaces, or any namespace you have registered — and may name at most two; any other key (including the account identifier or `userId`) is rejected. If you use a scoped credential, the profile's effective scopes may not exceed your own; a root API key (`sk_`) is exempt. Requires the `profiles:c` scope.
2857
+ * Creates a new access profile under the given app context. This call is idempotent by `principalId`: if a profile with the same `principalId` already exists, the existing profile is returned (with status 200) instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing profile was returned) tells the two apart. To overwrite an existing profile's `scopes`/`roleId`, `identityOverrides`, and `status` instead of returning it unchanged, set `?upsert=true` (this also requires the `profiles:u` scope, and applies the same `identityOverrides` bounds the update endpoint documents — a scoped credential may not repoint or clear an identity value it does not itself hold). You must provide exactly one of `scopes` (an inline list of scopes) or `roleId` (a reference to a role); supplying both, or neither, is rejected. `identityOverrides` is keyed by ownership namespace in `scope:<namespace>` form — `scope:org` and `scope:client` for the reserved namespaces, or any namespace you have registered — and may name at most two; any other key (including the account identifier or `userId`) is rejected. If you use a scoped credential, the profile's effective scopes may not exceed your own; a root API key (`sk_`) is exempt. Requires the `profiles:c` scope.
2858
2858
  *
2859
2859
  * @param {Vectros.CreateAccessProfileRequest} request
2860
2860
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -3272,7 +3272,7 @@ var AuthClient = class {
3272
3272
  );
3273
3273
  }
3274
3274
  /**
3275
- * Updates an access profile. This is a partial update: any field you omit (or send as null) keeps its existing value. A profile must reference either inline `scopes` or a `roleId`, never both — so setting `scopes` clears any `roleId`, and setting `roleId` clears any inline `scopes`. The `contextId` and `principalId` are immutable. Status changes (for example active to suspended) take effect within about five minutes. If you use a scoped credential, the profile's effective scopes may not exceed your own; a root API key (`sk_`) is exempt. Requires the `profiles:u` scope.
3275
+ * Updates an access profile. This is a partial update: any field you omit (or send as null) keeps its existing value. A profile must reference either inline `scopes` or a `roleId`, never both — so setting `scopes` clears any `roleId`, and setting `roleId` clears any inline `scopes`. The `contextId` and `principalId` are immutable. Status changes (for example active to suspended) take effect within about five minutes. If you use a scoped credential, the profile's effective scopes may not exceed your own, and its `identityOverrides` are bounded twice: you may only set a value your own identity holds, and you may only change or clear a value the profile already holds if that value is yours as well. Repointing or clearing another principal's established identity therefore returns 403. A root API key (`sk_`) is exempt. Requires the `profiles:u` scope.
3276
3276
  *
3277
3277
  * @param {Vectros.UpdateAccessProfileRequest} request
3278
3278
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -3345,7 +3345,7 @@ var AuthClient = class {
3345
3345
  );
3346
3346
  }
3347
3347
  /**
3348
- * Deletes an access profile. Within about five minutes (the access-profile cache lifetime), token minting for this principal in this context will be denied. Requires the `profiles:d` scope.
3348
+ * Deletes an access profile. Within about five minutes (the access-profile cache lifetime), token minting for this principal in this context will be denied. If you use a scoped credential and the profile carries `identityOverrides`, you may only delete it when you hold those values yourself — deleting a profile removes its identity, so the same bound applies as when clearing it. A profile with no `identityOverrides` is unaffected, and a root API key (`sk_`) is exempt. Requires the `profiles:d` scope.
3349
3349
  *
3350
3350
  * @param {Vectros.DeleteAccessProfileRequest} request
3351
3351
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -4199,7 +4199,7 @@ var AuthClient = class {
4199
4199
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token");
4200
4200
  }
4201
4201
  /**
4202
- * Invite a new member to one of your app contexts by email. Creates a pending user with a pre-resolved access profile (their permissions on accept) and signs an invitation token. This call is idempotent on the combination of context and email: re-inviting the same email in the same context rotates the token and resends the invitation rather than creating a duplicate. Returns HTTP 201 on a new invite or a successful resend. Returns 409 if that email already belongs to an active or suspended member of the app context, or already has an identity elsewhere in your account (an email can currently belong to only one tenant per account, i.e. your test and live environments cannot share an email). When `sendEmail` is false, the response includes the raw token and a ready-to-use accept link so you can deliver the invitation through your own email provider. Requires the `admin:users` scope.
4202
+ * Invite a new member to one of your app contexts by email. Creates a pending user with a pre-resolved access profile (their permissions on accept) and signs an invitation token. This call is idempotent on the combination of context and email: re-inviting the same email in the same context rotates the token and resends the invitation rather than creating a duplicate — this requires the `users:r` and `users:u` scopes in addition to `users:c`, because resending rotates a credential on an existing invitation and invalidates any link already sent. Without them the collision returns 409 instead, with no invitation details and no change to the outstanding invitation. Returns HTTP 201 on a new invite or a successful resend. Returns 409 if that email already belongs to an active or suspended member of the app context, or already has an identity elsewhere in your account (an email can currently belong to only one tenant per account, i.e. your test and live environments cannot share an email). When `sendEmail` is false, the response includes the raw token and a ready-to-use accept link so you can deliver the invitation through your own email provider. Requires the `users:c` scope.
4203
4203
  *
4204
4204
  * @param {Vectros.CreateInviteRequest} request
4205
4205
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -4270,7 +4270,7 @@ var AuthClient = class {
4270
4270
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite");
4271
4271
  }
4272
4272
  /**
4273
- * Resend an outstanding invitation, identified by its email and app context. Rotates the invitation token and extends its expiry, then (when `sendEmail` is true) re-delivers the email. Rotating the token invalidates any previously issued link for this invitation, so only the newest link works. The invitee's pending permissions are left unchanged. Requires the `admin:users` scope.
4273
+ * Resend an outstanding invitation, identified by its email and app context. Rotates the invitation token and extends its expiry, then (when `sendEmail` is true) re-delivers the email. Rotating the token invalidates any previously issued link for this invitation, so only the newest link works. The invitee's pending permissions are left unchanged. Because this rotates a credential on an existing invitation, it requires the `users:c`, `users:r` and `users:u` scopes.
4274
4274
  *
4275
4275
  * @param {Vectros.CreateInviteRequest} request
4276
4276
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -4604,7 +4604,7 @@ var DocumentsClient = class {
4604
4604
  * userId: "550e8400-e29b-41d4-a716-446655440000",
4605
4605
  * scope: "group:eng-team",
4606
4606
  * folderId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
4607
- * startFrom: "doc_prev123"
4607
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
4608
4608
  * })
4609
4609
  */
4610
4610
  listDocuments(request = {}, requestOptions) {
@@ -4914,6 +4914,7 @@ var DocumentsClient = class {
4914
4914
  * @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
4915
4915
  *
4916
4916
  * @throws {@link Vectros.BadRequestError}
4917
+ * @throws {@link Vectros.ForbiddenError}
4917
4918
  * @throws {@link Vectros.NotFoundError}
4918
4919
  * @throws {@link Vectros.ConflictError}
4919
4920
  * @throws {@link Vectros.TooManyRequestsError}
@@ -4961,6 +4962,8 @@ var DocumentsClient = class {
4961
4962
  switch (_response.error.statusCode) {
4962
4963
  case 400:
4963
4964
  throw new BadRequestError(_response.error.body, _response.rawResponse);
4965
+ case 403:
4966
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
4964
4967
  case 404:
4965
4968
  throw new NotFoundError(_response.error.body, _response.rawResponse);
4966
4969
  case 409:
@@ -4991,7 +4994,7 @@ var DocumentsClient = class {
4991
4994
  * field: "po_number",
4992
4995
  * value: "PO-1001",
4993
4996
  * prefix: "PO-2024",
4994
- * startFrom: "550e8400-e29b-41d4-a716-446655440000",
4997
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl",
4995
4998
  * userId: "550e8400-e29b-41d4-a716-446655440000",
4996
4999
  * scope: "org:6ba7b810-9dad-11d1-80b4-00c04fd430c8"
4997
5000
  * })
@@ -5367,7 +5370,7 @@ var FoldersClient = class {
5367
5370
  * parentFolderId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
5368
5371
  * userId: "550e8400-e29b-41d4-a716-446655440000",
5369
5372
  * scope: "group:eng-team",
5370
- * startFrom: "fld_prev123"
5373
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
5371
5374
  * })
5372
5375
  */
5373
5376
  listFolders(request = {}, requestOptions) {
@@ -5544,6 +5547,7 @@ var FoldersClient = class {
5544
5547
  * @param {Vectros.UpdateFolderRequest} request
5545
5548
  * @param {FoldersClient.RequestOptions} requestOptions - Request-specific configuration.
5546
5549
  *
5550
+ * @throws {@link Vectros.ForbiddenError}
5547
5551
  * @throws {@link Vectros.NotFoundError}
5548
5552
  * @throws {@link Vectros.TooManyRequestsError}
5549
5553
  *
@@ -5588,6 +5592,8 @@ var FoldersClient = class {
5588
5592
  }
5589
5593
  if (_response.error.reason === "status-code") {
5590
5594
  switch (_response.error.statusCode) {
5595
+ case 403:
5596
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
5591
5597
  case 404:
5592
5598
  throw new NotFoundError(_response.error.body, _response.rawResponse);
5593
5599
  case 429:
@@ -5670,6 +5676,7 @@ var FoldersClient = class {
5670
5676
  * @param {FoldersClient.RequestOptions} requestOptions - Request-specific configuration.
5671
5677
  *
5672
5678
  * @throws {@link Vectros.BadRequestError}
5679
+ * @throws {@link Vectros.ForbiddenError}
5673
5680
  * @throws {@link Vectros.NotFoundError}
5674
5681
  * @throws {@link Vectros.ConflictError}
5675
5682
  * @throws {@link Vectros.TooManyRequestsError}
@@ -5717,6 +5724,8 @@ var FoldersClient = class {
5717
5724
  switch (_response.error.statusCode) {
5718
5725
  case 400:
5719
5726
  throw new BadRequestError(_response.error.body, _response.rawResponse);
5727
+ case 403:
5728
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
5720
5729
  case 404:
5721
5730
  throw new NotFoundError(_response.error.body, _response.rawResponse);
5722
5731
  case 409:
@@ -5804,7 +5813,8 @@ var IdentityClient = class {
5804
5813
  *
5805
5814
  * @example
5806
5815
  * await client.identity.listEntities({
5807
- * namespace: "team"
5816
+ * namespace: "team",
5817
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
5808
5818
  * })
5809
5819
  */
5810
5820
  listEntities(request, requestOptions) {
@@ -6568,7 +6578,7 @@ var IdentityClient = class {
6568
6578
  * @example
6569
6579
  * await client.identity.listUsers({
6570
6580
  * externalId: "usr_12345",
6571
- * startFrom: "550e8400-e29b-41d4-a716-446655440000",
6581
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl",
6572
6582
  * type: "person_v1",
6573
6583
  * field: "team",
6574
6584
  * value: "engineering",
@@ -6749,11 +6759,12 @@ var IdentityClient = class {
6749
6759
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/{id}");
6750
6760
  }
6751
6761
  /**
6752
- * Updates mutable fields on an existing user (such as email, status, payload, or schema binding). The `type` field is immutable after creation. This endpoint also activates an invited user: a PUT that moves a PENDING user to ACTIVE and carries `inviteToken`, `externalSubject`, and `emailVerifiedAttestation=true` completes the invitation. Requires the `users:u` scope.
6762
+ * Updates mutable fields on an existing user (such as email, status, payload, or schema binding). The `type` field is immutable after creation, and `email` cannot be changed while an invitation to that user is still outstanding — revoke the invitation, or invite the new address instead. This endpoint also activates an invited user: a PUT that moves a PENDING user to ACTIVE and carries `inviteToken`, `externalSubject`, and `emailVerifiedAttestation=true` completes the invitation. Requires the `users:u` scope.
6753
6763
  *
6754
6764
  * @param {Vectros.UpdateUserRequest} request
6755
6765
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
6756
6766
  *
6767
+ * @throws {@link Vectros.BadRequestError}
6757
6768
  * @throws {@link Vectros.NotFoundError}
6758
6769
  * @throws {@link Vectros.TooManyRequestsError}
6759
6770
  *
@@ -6798,6 +6809,8 @@ var IdentityClient = class {
6798
6809
  }
6799
6810
  if (_response.error.reason === "status-code") {
6800
6811
  switch (_response.error.statusCode) {
6812
+ case 400:
6813
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
6801
6814
  case 404:
6802
6815
  throw new NotFoundError(_response.error.body, _response.rawResponse);
6803
6816
  case 429:
@@ -7445,7 +7458,7 @@ var RecordsClient = class {
7445
7458
  * folderId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
7446
7459
  * userId: "550e8400-e29b-41d4-a716-446655440000",
7447
7460
  * scope: "group:eng-team",
7448
- * startFrom: "550e8400-e29b-41d4-a716-446655440000"
7461
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
7449
7462
  * })
7450
7463
  */
7451
7464
  listRecords(request = {}, requestOptions) {
@@ -7633,6 +7646,7 @@ var RecordsClient = class {
7633
7646
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
7634
7647
  *
7635
7648
  * @throws {@link Vectros.BadRequestError}
7649
+ * @throws {@link Vectros.ForbiddenError}
7636
7650
  * @throws {@link Vectros.NotFoundError}
7637
7651
  * @throws {@link Vectros.ConflictError}
7638
7652
  * @throws {@link Vectros.TooManyRequestsError}
@@ -7681,6 +7695,8 @@ var RecordsClient = class {
7681
7695
  switch (_response.error.statusCode) {
7682
7696
  case 400:
7683
7697
  throw new BadRequestError(_response.error.body, _response.rawResponse);
7698
+ case 403:
7699
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
7684
7700
  case 404:
7685
7701
  throw new NotFoundError(_response.error.body, _response.rawResponse);
7686
7702
  case 409:
@@ -7762,6 +7778,7 @@ var RecordsClient = class {
7762
7778
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
7763
7779
  *
7764
7780
  * @throws {@link Vectros.BadRequestError}
7781
+ * @throws {@link Vectros.ForbiddenError}
7765
7782
  * @throws {@link Vectros.NotFoundError}
7766
7783
  * @throws {@link Vectros.ConflictError}
7767
7784
  * @throws {@link Vectros.TooManyRequestsError}
@@ -7812,6 +7829,8 @@ var RecordsClient = class {
7812
7829
  switch (_response.error.statusCode) {
7813
7830
  case 400:
7814
7831
  throw new BadRequestError(_response.error.body, _response.rawResponse);
7832
+ case 403:
7833
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
7815
7834
  case 404:
7816
7835
  throw new NotFoundError(_response.error.body, _response.rawResponse);
7817
7836
  case 409:
@@ -7829,7 +7848,15 @@ var RecordsClient = class {
7829
7848
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PATCH", "/v1/records/{id}");
7830
7849
  }
7831
7850
  /**
7832
- * Finds records by the value of a lookup field declared on the type's schema. Provide exactly one lookup mode: `value` (exact match), `from`+`to` (inclusive range, ascending by value), or `prefix` (string fields only, ascending). Range and prefix lookups are not supported on a sensitive field, because its value is stored as a blind index and has no sortable order. An exact-`value` lookup on a sensitive field is also rejected on this GET endpoint — the value must not appear in the URL — so use the `POST /v1/records/lookup` body variant for sensitive fields. Results are paginated: set `limit` for the page size and pass the returned `nextCursor` back as `startFrom` for the next page. Requires the `records:r:<type>` scope.
7851
+ * Finds records by the value of a lookup field declared on the type's schema. Provide exactly one lookup mode: `value` (exact match), `from`+`to` (inclusive range, ascending by value), or `prefix` (string fields only, ascending). Range and prefix lookups are not supported on a sensitive field, because its value is stored as a blind index and has no sortable order. An exact-`value` lookup on a sensitive field is also rejected on this GET endpoint — the value must not appear in the URL — so use the `POST /v1/records/lookup` body variant for sensitive fields.
7852
+ *
7853
+ * A `value` lookup can additionally be narrowed to a window of the lookup field's **sort key** using `sortFrom` and/or `sortTo` (inclusive) — for example, one session's records created since a timestamp. The sort key is whatever the schema declares as that lookup's `sortBy` (`createdAt` by default, `lastUpdated`, or another field), and bounds are given in that field's own units — epoch milliseconds for the two timestamp options. **Records that have no value for the sorted field are never included in a bounded window** — they are ordered ahead of every record that does have one, and a `sortFrom`/`sortTo` window only ever selects from records carrying a value. The sorted field does not have to be `required`.
7854
+ *
7855
+ * Narrowing is available on any lookup field your schema declares for fast equality lookup, and on `externalId` — which is always ordered by creation time, so its bounds are epoch milliseconds whatever the schema says. It is rejected (`400`) for a field declared with `rangeEnabled`, for a field declared beyond the schema's fast-lookup budget, for a lookup whose `sortBy` names a sensitive field (a sensitive value is stored as a blind index and has no order), and for a window whose start is after its end. Ownership fields are not lookup fields on this endpoint at all — see the `field` parameter.
7856
+ *
7857
+ * While paging a narrowed lookup, keep every other parameter identical. The cursor is valid only for the exact query that returned it — changing `order`, `sortFrom`, `sortTo`, or dropping them altogether, is rejected rather than silently resumed at a position that means something different in the new query.
7858
+ *
7859
+ * Results are paginated: set `limit` for the page size and pass the returned `nextCursor` back as `startFrom` for the next page. **Keep paging until `nextCursor` is null** — a page can come back empty or shorter than `limit` while more results remain, so an empty page is not the end of the results. Requires the `records:r:<type>` scope.
7833
7860
  *
7834
7861
  * @param {Vectros.LookupRecordsRequest} request
7835
7862
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7841,22 +7868,40 @@ var RecordsClient = class {
7841
7868
  * type: "intake_form",
7842
7869
  * field: "email",
7843
7870
  * value: "jane@example.com",
7871
+ * values: ["open"],
7844
7872
  * prefix: "jane",
7845
- * startFrom: "550e8400-e29b-41d4-a716-446655440000"
7873
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
7846
7874
  * })
7847
7875
  */
7848
7876
  lookupRecords(request, requestOptions) {
7849
7877
  return HttpResponsePromise.fromPromise(this.__lookupRecords(request, requestOptions));
7850
7878
  }
7851
7879
  async __lookupRecords(request, requestOptions) {
7852
- const { type: type_, field, value, from: from_, to, prefix, startFrom, limit, includePayload, order } = request;
7880
+ const {
7881
+ type: type_,
7882
+ field,
7883
+ value,
7884
+ values,
7885
+ from: from_,
7886
+ to,
7887
+ prefix,
7888
+ sortFrom,
7889
+ sortTo,
7890
+ startFrom,
7891
+ limit,
7892
+ includePayload,
7893
+ order
7894
+ } = request;
7853
7895
  const _queryParams = {
7854
7896
  type: type_,
7855
7897
  field,
7856
7898
  value,
7899
+ values,
7857
7900
  from: from_,
7858
7901
  to,
7859
7902
  prefix,
7903
+ sortFrom,
7904
+ sortTo,
7860
7905
  startFrom,
7861
7906
  limit,
7862
7907
  includePayload,
@@ -7900,7 +7945,7 @@ var RecordsClient = class {
7900
7945
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/records/lookup");
7901
7946
  }
7902
7947
  /**
7903
- * Body-based equivalent of `GET /v1/records/lookup`. Use this when looking up by a sensitive field: the value travels in the request body (and is blind-indexed server-side) instead of in the URL query string, so it never lands in access, CDN, or proxy logs. The GET variant rejects an exact-value lookup on a sensitive field and directs you here. Non-sensitive exact-value, range (`from`+`to`), and prefix lookups also work here. Returns the same `{data, nextCursor}` envelope and uses the same pagination as the GET variant. Requires the `records:r:<type>` scope.
7948
+ * Body-based equivalent of `GET /v1/records/lookup`. Use this when looking up by a sensitive field: the value travels in the request body (and is blind-indexed server-side) instead of in the URL query string, so it never lands in access, CDN, or proxy logs. The GET variant rejects an exact-value lookup on a sensitive field and directs you here. Non-sensitive exact-value, range (`from`+`to`), and prefix lookups also work here, as does narrowing a `value` lookup by the field's sort key with `sortFrom`/`sortTo` — see the GET variant for what the sort key is and when narrowing by it is available. Returns the same `{data, nextCursor}` envelope and uses the same pagination as the GET variant; keep paging until `nextCursor` is null rather than stopping on an empty page. Requires the `records:r:<type>` scope.
7904
7949
  *
7905
7950
  * @param {Vectros.RecordLookupRequest} request
7906
7951
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8095,7 +8140,7 @@ var SchemasClient = class {
8095
8140
  * scope: "org:6ba7b810-9dad-11d1-80b4-00c04fd430c8",
8096
8141
  * surface: "document",
8097
8142
  * recordType: "intake_form",
8098
- * startFrom: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
8143
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
8099
8144
  * })
8100
8145
  */
8101
8146
  listSchemas(request = {}, requestOptions) {
@@ -8299,6 +8344,7 @@ var SchemasClient = class {
8299
8344
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
8300
8345
  *
8301
8346
  * @throws {@link Vectros.BadRequestError}
8347
+ * @throws {@link Vectros.ForbiddenError}
8302
8348
  * @throws {@link Vectros.NotFoundError}
8303
8349
  * @throws {@link Vectros.TooManyRequestsError}
8304
8350
  *
@@ -8347,6 +8393,8 @@ var SchemasClient = class {
8347
8393
  switch (_response.error.statusCode) {
8348
8394
  case 400:
8349
8395
  throw new BadRequestError(_response.error.body, _response.rawResponse);
8396
+ case 403:
8397
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
8350
8398
  case 404:
8351
8399
  throw new NotFoundError(_response.error.body, _response.rawResponse);
8352
8400
  case 429:
@@ -8659,8 +8707,8 @@ function warnOnSuboptimalKey(key, log) {
8659
8707
 
8660
8708
  // src/build-info.ts
8661
8709
  var BUILD_INFO = {
8662
- mcpServer: true ? "0.12.0" : "dev",
8663
- sdk: true ? "0.37.0" : "dev"
8710
+ mcpServer: true ? "0.13.0" : "dev",
8711
+ sdk: true ? "0.38.0" : "dev"
8664
8712
  };
8665
8713
 
8666
8714
  // src/log.ts
@@ -8700,7 +8748,9 @@ async function resolveIdentity({
8700
8748
  const derived = {
8701
8749
  status: "ok",
8702
8750
  environment: deriveEnvironment(environment),
8703
- principalType: derivePrincipalType(apiKey)
8751
+ principalType: derivePrincipalType(apiKey),
8752
+ mcpServerVersion: BUILD_INFO.mcpServer,
8753
+ sdkVersion: BUILD_INFO.sdk
8704
8754
  };
8705
8755
  if (!apiKey || !environment) {
8706
8756
  log.debug({ component: "identity", mode: "derived-only" }, "identity derived (no fetch)");
@@ -8728,7 +8778,13 @@ async function resolveIdentity({
8728
8778
  );
8729
8779
  }
8730
8780
  }
8731
- const merged = { ...derived, ...extended, status: "ok" };
8781
+ const merged = {
8782
+ ...derived,
8783
+ ...extended,
8784
+ status: "ok",
8785
+ mcpServerVersion: derived.mcpServerVersion,
8786
+ sdkVersion: derived.sdkVersion
8787
+ };
8732
8788
  log.debug(
8733
8789
  {
8734
8790
  component: "identity",
@@ -9050,14 +9106,23 @@ var import_zod2 = require("zod");
9050
9106
  function pageItems(page) {
9051
9107
  return page.data ?? [];
9052
9108
  }
9109
+ var MAX_PAGES = 100;
9110
+ var MAX_ROWS = 1e6;
9053
9111
  async function drainPages(fetchPage) {
9054
9112
  const all = [];
9055
9113
  let startFrom;
9114
+ let pages = 0;
9056
9115
  for (; ; ) {
9057
9116
  const page = await fetchPage(startFrom);
9058
- if (page.data?.length) all.push(...page.data);
9117
+ pages++;
9118
+ if (page.data?.length) for (const item of page.data) all.push(item);
9059
9119
  const next = page.nextCursor;
9060
9120
  if (!next) break;
9121
+ if (pages > MAX_PAGES || all.length > MAX_ROWS) {
9122
+ throw new Error(
9123
+ `drainPages: exceeded the runaway guard (${all.length} rows over ${pages} pages, limits ${MAX_ROWS}/${MAX_PAGES}) without the cursor going null. Refusing to return a partial result \u2014 the backend may be returning empty pages with a live cursor.`
9124
+ );
9125
+ }
9061
9126
  startFrom = next;
9062
9127
  }
9063
9128
  return all;
@@ -9068,14 +9133,23 @@ var MCP_DEFAULT_LIMIT2 = 3;
9068
9133
  var MCP_MAX_LIMIT2 = 100;
9069
9134
  var inputSchema2 = {
9070
9135
  type: import_zod2.z.string().min(1, "type (record type) is required").describe('Record type / schema name (e.g. "patient", "clinical_note").'),
9071
- // Lookup-mode args — provide `field` plus EXACTLY ONE of: value | from+to | prefix.
9136
+ // Lookup-mode args — provide `field` plus EXACTLY ONE of: value | values | from+to | prefix.
9072
9137
  field: import_zod2.z.string().optional().describe(
9073
- 'Lookup mode: name of the lookup-indexed field to query by. `externalId` is always queryable \u2014 e.g. `{type:"control", field:"externalId", value:"ctrl-scoped-key"}` finds the record you created under that externalId. (record_get / record_update also accept externalId directly.)'
9138
+ 'Lookup mode: name of the lookup-indexed field to query by. `externalId` is always queryable \u2014 e.g. `{type:"control", field:"externalId", value:"ctrl-scoped-key"}` finds the record you created under that externalId. (record_get / record_update also accept externalId directly.) For a SCHEMA-declared composite lookup \u2014 check list_schemas\' lookupFields for a fieldNames entry, not any two fields you pick \u2014 give those field names comma-joined in the declared order (e.g. "status,area"). Use `values` to match on several of those fields at once, or `value` to match on just the FIRST field named (equivalent to a one-element `values`); range/prefix are not valid on a composite field.'
9139
+ ),
9140
+ value: import_zod2.z.string().optional().describe(
9141
+ "Lookup mode (equality): exact-match value for `field`. Also valid when `field` names a SCHEMA-declared composite lookup \u2014 matches just the first named field, grouping by the rest (equivalent to `values` with one entry)."
9142
+ ),
9143
+ values: import_zod2.z.array(import_zod2.z.string()).min(1).max(3).optional().describe(
9144
+ 'Lookup mode (composite equality): only valid against a lookup the SCHEMA declares over several fields together (a fieldNames entry in list_schemas \u2014 you cannot combine two ordinary single-field lookups this way). One value per field, in the order the schema declares them (e.g. field="status,area", values=["open","billing"]). Mutually exclusive with `value`; a single-element array is exactly `value` written uniformly. Supplying FEWER values than the lookup declares is allowed \u2014 as long as they\'re a leading run of its fields \u2014 and returns records grouped by the fields left unspecified; `order` then sorts within each group, not across them. Record lookups only (not documents/users/entities).'
9074
9145
  ),
9075
- value: import_zod2.z.string().optional().describe("Lookup mode (equality): exact-match value for `field`."),
9076
9146
  from: import_zod2.z.string().optional().describe("Lookup mode (range): inclusive lower bound; requires `to`. Non-sensitive fields only."),
9077
9147
  to: import_zod2.z.string().optional().describe("Lookup mode (range): inclusive upper bound; requires `from`."),
9078
9148
  prefix: import_zod2.z.string().optional().describe("Lookup mode (prefix): match values starting with this. String, non-sensitive fields only."),
9149
+ sortFrom: import_zod2.z.string().optional().describe(
9150
+ "Lookup mode (equality/composite only): inclusive lower bound on the lookup field's sort key, narrowing a `value`/`values` match to a window instead of paging the whole match. Give the bound in the sort field's own units (epoch milliseconds for `createdAt`/`lastUpdated`). With `values`, requires every value be supplied (no grouping) for one continuous ordering. Not valid with `from`/`to`/`prefix`."
9151
+ ),
9152
+ sortTo: import_zod2.z.string().optional().describe("Lookup mode (equality/composite only): inclusive upper bound on the sort key; pairs with `sortFrom`."),
9079
9153
  order: import_zod2.z.enum(["asc", "desc"]).optional().describe(
9080
9154
  "Lookup mode: sort direction by the looked-up field. `asc` (default) or `desc` \u2014 use `desc` with a range/prefix lookup to get the most-recent / highest values first (e.g. latest-N). Ignored in list mode."
9081
9155
  ),
@@ -9092,54 +9166,106 @@ var inputSchema2 = {
9092
9166
  var recordQuery = ({ client, log }) => ({
9093
9167
  name: "record_query",
9094
9168
  title: "Record query (list or lookup)",
9095
- description: "Query structured records of a given type. Two modes:\n \u2022 Lookup on a lookup-indexed field: pass `field` plus exactly one of `value` (exact), `from`+`to` (range), or `prefix`. Works on sensitive fields too.\n \u2022 List by type: omit `field`; optionally filter by `userId` or `scope` (`namespace:value`).\nReturns a `{ data, nextCursor }` page: `data` holds up to `limit` records (default 3, max 100 \u2014 raise it in one call when you can accept the payload), and a non-null `nextCursor` means more remain \u2014 pass it back as `startFrom` to page. Mode is auto-detected from the arguments present.",
9169
+ description: "Query structured records of a given type. Two modes:\n \u2022 Lookup on a lookup-indexed field: pass `field` plus exactly one of `value` (exact), `values` (exact, composite \u2014 one value per field; a single-element array is the same as `value`), `from`+`to` (range), or `prefix`. `values` with more than one entry, or a comma-joined `field`, ONLY works for a lookup the schema itself declared over several fields together \u2014 check list_schemas' lookupFields for a fieldNames entry, then pass those field names comma-joined as `field`; `from`/`to`/`prefix` are not valid there. Works on sensitive fields too. Optionally narrow an exact match to a window with `sortFrom`/`sortTo`.\n \u2022 List by type: omit `field`; optionally filter by `userId` or `scope` (`namespace:value`).\nReturns a `{ data, nextCursor }` page: `data` holds up to `limit` records (default 3, max 100 \u2014 raise it in one call when you can accept the payload), and a non-null `nextCursor` means more remain \u2014 pass it back as `startFrom` to page. A composite lookup given fewer than its full field count returns records grouped by the unspecified fields, ordered within each group. Mode is auto-detected from the arguments present.",
9096
9170
  inputSchema: inputSchema2,
9097
9171
  handler: async (args) => {
9098
9172
  const limit = args.limit ?? MCP_DEFAULT_LIMIT2;
9099
9173
  const type = args.type;
9100
9174
  const field = args.field;
9101
9175
  const value = args.value;
9176
+ const values = args.values;
9102
9177
  const from = args.from;
9103
9178
  const to = args.to;
9104
9179
  const prefix = args.prefix;
9180
+ const sortFrom = args.sortFrom;
9181
+ const sortTo = args.sortTo;
9105
9182
  const startFrom = args.startFrom;
9183
+ if (!field && (value !== void 0 || values !== void 0 || from !== void 0 || to !== void 0 || prefix !== void 0 || sortFrom !== void 0 || sortTo !== void 0)) {
9184
+ return toolError(
9185
+ "record_query",
9186
+ new Error(
9187
+ "'value'/'values'/'from'/'to'/'prefix'/'sortFrom'/'sortTo' require 'field' \u2014 without it, list mode runs and silently ignores them. Pass 'field' to run a lookup, or drop these to list by type."
9188
+ )
9189
+ );
9190
+ }
9106
9191
  try {
9107
9192
  let page;
9108
9193
  if (field) {
9194
+ const fieldNames = field.split(",").map((f) => f.trim()).filter(Boolean);
9195
+ const normalizedField = fieldNames.join(",");
9196
+ const isCompositeField = fieldNames.length > 1;
9109
9197
  const hasEquality = value !== void 0;
9198
+ const hasComposite = values !== void 0;
9110
9199
  const hasRange = from !== void 0 || to !== void 0;
9111
9200
  const hasPrefix = prefix !== void 0;
9112
- const modes = Number(hasEquality) + Number(hasRange) + Number(hasPrefix);
9201
+ const modes = Number(hasEquality) + Number(hasComposite) + Number(hasRange) + Number(hasPrefix);
9113
9202
  if (modes === 0) {
9114
9203
  return toolError(
9115
9204
  "record_query",
9116
9205
  new Error(
9117
- `lookup on field '${field}' needs one of: 'value' (exact), 'from'+'to' (range), or 'prefix'.`
9206
+ `lookup on field '${field}' needs one of: 'value' (exact), 'values' (composite exact), 'from'+'to' (range), or 'prefix'.`
9118
9207
  )
9119
9208
  );
9120
9209
  }
9121
9210
  if (modes > 1) {
9122
9211
  return toolError(
9123
9212
  "record_query",
9124
- new Error("'value', 'from'/'to', and 'prefix' are mutually exclusive \u2014 provide exactly one.")
9213
+ new Error(
9214
+ "'value', 'values', 'from'/'to', and 'prefix' are mutually exclusive \u2014 provide exactly one."
9215
+ )
9125
9216
  );
9126
9217
  }
9127
9218
  if (hasRange && !(from !== void 0 && to !== void 0)) {
9128
9219
  return toolError("record_query", new Error("range lookup requires both 'from' and 'to'."));
9129
9220
  }
9221
+ if (isCompositeField && (hasRange || hasPrefix)) {
9222
+ return toolError(
9223
+ "record_query",
9224
+ new Error(
9225
+ `'field' names ${fieldNames.length} fields ('${normalizedField}') \u2014 a composite lookup is exact-match only ('value' or 'values'), not 'from'/'to'/'prefix'.`
9226
+ )
9227
+ );
9228
+ }
9229
+ if (hasComposite && values.length > fieldNames.length) {
9230
+ return toolError(
9231
+ "record_query",
9232
+ new Error(
9233
+ `'values' has ${values.length} entries but 'field' names only ${fieldNames.length} field(s) ('${normalizedField}') \u2014 supply at most one value per field, in order (fewer only as a leading run).`
9234
+ )
9235
+ );
9236
+ }
9237
+ if (sortFrom !== void 0 || sortTo !== void 0) {
9238
+ if (!(hasEquality || hasComposite)) {
9239
+ return toolError(
9240
+ "record_query",
9241
+ new Error("'sortFrom'/'sortTo' narrow a 'value' or 'values' match only \u2014 not 'from'/'to'/'prefix'.")
9242
+ );
9243
+ }
9244
+ if (hasComposite && values.length < fieldNames.length) {
9245
+ return toolError(
9246
+ "record_query",
9247
+ new Error(
9248
+ `'sortFrom'/'sortTo' with 'values' requires a value for every field named in 'field' (got ${values.length} of ${fieldNames.length}) \u2014 a partial/grouped match has no single continuous ordering to window.`
9249
+ )
9250
+ );
9251
+ }
9252
+ }
9130
9253
  page = await client.records.lookupRecordsByBody({
9131
9254
  type,
9132
- field,
9255
+ field: normalizedField,
9133
9256
  value,
9257
+ values,
9134
9258
  from,
9135
9259
  to,
9136
9260
  prefix,
9261
+ sortFrom,
9262
+ sortTo,
9137
9263
  order: args.order,
9138
9264
  startFrom,
9139
9265
  limit
9140
9266
  });
9141
9267
  log.debug(
9142
- { tool: "record_query", mode: "lookup", type, field, returned: pageItems(page).length },
9268
+ { tool: "record_query", mode: "lookup", type, field: normalizedField, returned: pageItems(page).length },
9143
9269
  "record_query lookup ok"
9144
9270
  );
9145
9271
  } else {
@@ -10078,7 +10204,7 @@ var inputSchema18 = {
10078
10204
  var currentIdentity = ({ log, apiKey, environment }) => ({
10079
10205
  name: "current_identity",
10080
10206
  title: "Current identity (tenant + principal scope)",
10081
- description: "Describe the credential the MCP server is operating under. Returns tenantId, environment (staging|production), principalType (root_key|scoped_key|token), principalKeyId, principalLabel, and (for scoped credentials) allowedActions plus dataScope \u2014 the credential's reach as { userId, scopes[] }, where each scope is a `namespace:value` string (e.g. \"org:<uuid>\"). Use this when the user asks 'what can you do here?' or 'what tenant am I in?'. Calls GET /v1/ping under the hood.",
10207
+ description: "Describe the credential the MCP server is operating under. Returns tenantId, environment (staging|production), principalType (root_key|scoped_key|token), principalKeyId, principalLabel, and (for scoped credentials) allowedActions plus dataScope \u2014 the credential's reach as { userId, scopes[] }, where each scope is a `namespace:value` string (e.g. \"org:<uuid>\"). Also reports mcpServerVersion (this server's own package version) and sdkVersion (the bundled @vectros-ai/sdk version) so a caller can tell what build it's talking to. Use this when the user asks 'what can you do here?', 'what tenant am I in?', or 'what version is this?'. Calls GET /v1/ping under the hood.",
10082
10208
  inputSchema: inputSchema18,
10083
10209
  handler: async () => {
10084
10210
  try {
@@ -10556,7 +10682,7 @@ var identityResource = ({ log, apiKey, environment }) => ({
10556
10682
  name: "identity",
10557
10683
  uri: "vectros://identity",
10558
10684
  title: "Current identity (tenant + principal scope)",
10559
- description: "The tenant + principal scope the MCP server is authenticated as. Returns the same payload as the `current_identity` tool: status, tenantId, environment, principalType, principalKeyId, principalLabel, and (for scoped credentials) allowedActions + dataScope ({ userId, scopes[] }). The /v1/ping backing call ships this extended shape today.",
10685
+ description: "The tenant + principal scope the MCP server is authenticated as. Returns the same payload as the `current_identity` tool: status, tenantId, environment, principalType, principalKeyId, principalLabel, (for scoped credentials) allowedActions + dataScope ({ userId, scopes[] }), plus mcpServerVersion and sdkVersion (this build's own version and the bundled @vectros-ai/sdk version). The /v1/ping backing call ships the extended shape today; the version fields are always client-derived.",
10560
10686
  mimeType: "application/json",
10561
10687
  read: async () => {
10562
10688
  const identity = await resolveIdentity({ log, apiKey, environment });