@vectros-ai/mcp-server 0.5.3 → 0.7.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/CHANGELOG.md +75 -0
- package/README.md +17 -2
- package/dist/cli-http.js +212 -46
- package/dist/cli-http.js.map +1 -1
- package/dist/cli-http.mjs +212 -46
- package/dist/cli-http.mjs.map +1 -1
- package/dist/cli.js +212 -46
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +212 -46
- package/dist/cli.mjs.map +1 -1
- package/dist/index.js +212 -46
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +212 -46
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/cli-http.mjs
CHANGED
|
@@ -481,10 +481,18 @@ var DocumentRequest;
|
|
|
481
481
|
Text: "TEXT",
|
|
482
482
|
None: "NONE"
|
|
483
483
|
};
|
|
484
|
+
DocumentRequest2.Status = {
|
|
485
|
+
Active: "ACTIVE",
|
|
486
|
+
Archived: "ARCHIVED"
|
|
487
|
+
};
|
|
484
488
|
})(DocumentRequest || (DocumentRequest = {}));
|
|
485
489
|
var DocumentResponse;
|
|
486
490
|
((DocumentResponse2) => {
|
|
487
491
|
DocumentResponse2.Status = {
|
|
492
|
+
Active: "ACTIVE",
|
|
493
|
+
Archived: "ARCHIVED"
|
|
494
|
+
};
|
|
495
|
+
DocumentResponse2.IndexStatus = {
|
|
488
496
|
PendingUpload: "PENDING_UPLOAD",
|
|
489
497
|
Uploaded: "UPLOADED",
|
|
490
498
|
Extracting: "EXTRACTING",
|
|
@@ -4890,7 +4898,7 @@ var DocumentsClient = class {
|
|
|
4890
4898
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/documents/{id}");
|
|
4891
4899
|
}
|
|
4892
4900
|
/**
|
|
4893
|
-
* Partially updates a document using an RFC 7386 JSON Merge Patch. The `payload` object is deep-merged: keys you send overwrite existing values (recursing into nested objects), a key set to `null` is deleted, and keys you omit are preserved — unlike PUT, which replaces the whole payload. Top-level fields (`title`, `
|
|
4901
|
+
* Partially updates a document using an RFC 7386 JSON Merge Patch. The `payload` object is deep-merged: keys you send overwrite existing values (recursing into nested objects), a key set to `null` is deleted, and keys you omit are preserved — unlike PUT, which replaces the whole payload. Top-level fields (`title`, `folderId`, `schemaId`, ownership) are set when present and left unchanged when omitted; sending a top-level field as `null` is rejected. Supplying `text` re-ingests the document body (same as PUT). `indexMode`, `externalId`, and `storeText` (text retention is fixed at ingest) are immutable and rejected if present. The merged result is validated against the bound schema. Pass `expectedVersion` for optimistic concurrency (409 on conflict). Requires the `documents:u` scope.
|
|
4894
4902
|
*
|
|
4895
4903
|
* @param {Vectros.PatchDocumentRequest} request
|
|
4896
4904
|
* @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5142,7 +5150,7 @@ var DocumentsClient = class {
|
|
|
5142
5150
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/documents/{id}/download");
|
|
5143
5151
|
}
|
|
5144
5152
|
/**
|
|
5145
|
-
* Returns the full
|
|
5153
|
+
* Returns the document's full text body when it is retained: always available for text-ingested documents, and for file-uploaded documents unless they were uploaded with `storeText=false` (which discards the extracted text once indexing completes — the original file remains available via `GET /{id}/download`). Returns 404 when the document does not exist, its text was not retained, or extraction has not yet completed. Requires the `documents:r` scope.
|
|
5146
5154
|
*
|
|
5147
5155
|
* @param {Vectros.GetDocumentTextRequest} request
|
|
5148
5156
|
* @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5255,7 +5263,7 @@ var DocumentsClient = class {
|
|
|
5255
5263
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/documents/{id}/versions");
|
|
5256
5264
|
}
|
|
5257
5265
|
/**
|
|
5258
|
-
* Starts a file-based document by returning a short-lived presigned S3 PUT URL. Upload the file bytes directly to `uploadUrl`; the document is then automatically queued for text extraction and asynchronous indexing. Supplying an `externalId` makes this idempotent — re-initiating an upload with the same `externalId` re-issues a fresh presigned URL to the SAME existing document/object (so a re-upload inherently replaces the file body) rather than creating a duplicate. The response's `created` field (and the HTTP status — 201 when a new document was minted, 200 when an existing one was matched) tells the two apart. With `?upsert=true`, the submitted `payload`/`title` are also applied to the matched document (file-body divergence cannot be diffed at upload-init — the bytes have not arrived yet — so the re-upload itself replaces the body
|
|
5266
|
+
* Starts a file-based document by returning a short-lived presigned S3 PUT URL. Upload the file bytes directly to `uploadUrl`; the document is then automatically queued for text extraction and asynchronous indexing. Supplying an `externalId` makes this idempotent — re-initiating an upload with the same `externalId` re-issues a fresh presigned URL to the SAME existing document/object (so a re-upload inherently replaces the file body) rather than creating a duplicate. The response's `created` field (and the HTTP status — 201 when a new document was minted, 200 when an existing one was matched) tells the two apart. With `?upsert=true`, the submitted `payload`/`title` are also applied to the matched document (file-body divergence cannot be diffed at upload-init — the bytes have not arrived yet — so the re-upload itself replaces the body). Creating a NEW document requires the `documents:c` scope. Re-uploading over an EXISTING document overwrites (and re-indexes) its body, so it is an update: it requires the `documents:u` scope (as does `?upsert=true` for the metadata).
|
|
5259
5267
|
*
|
|
5260
5268
|
* @param {Vectros.FileUploadRequest} request
|
|
5261
5269
|
* @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8756,8 +8764,8 @@ function warnOnSuboptimalKey(key, log) {
|
|
|
8756
8764
|
|
|
8757
8765
|
// src/build-info.ts
|
|
8758
8766
|
var BUILD_INFO = {
|
|
8759
|
-
mcpServer: true ? "0.
|
|
8760
|
-
sdk: true ? "0.
|
|
8767
|
+
mcpServer: true ? "0.7.0" : "dev",
|
|
8768
|
+
sdk: true ? "0.33.0" : "dev"
|
|
8761
8769
|
};
|
|
8762
8770
|
|
|
8763
8771
|
// src/log.ts
|
|
@@ -8994,6 +9002,32 @@ ${detail}${pointer}`
|
|
|
8994
9002
|
// src/tools/hybrid_search.ts
|
|
8995
9003
|
var MCP_DEFAULT_LIMIT = 3;
|
|
8996
9004
|
var MCP_MAX_LIMIT = 10;
|
|
9005
|
+
var MCP_RESPONSE_CHAR_BUDGET = 24e3;
|
|
9006
|
+
var SNIPPET_MAX_CHARS = 160;
|
|
9007
|
+
var INTERNAL_METADATA_KEYS = /* @__PURE__ */ new Set(["tenantId", "owner_id", "model_type", "rootFolderId", "folderId"]);
|
|
9008
|
+
function trimMetadata(md) {
|
|
9009
|
+
if (!md || typeof md !== "object" || Array.isArray(md)) return md;
|
|
9010
|
+
const out = {};
|
|
9011
|
+
for (const [k, v] of Object.entries(md)) {
|
|
9012
|
+
if (!INTERNAL_METADATA_KEYS.has(k)) out[k] = v;
|
|
9013
|
+
}
|
|
9014
|
+
return out;
|
|
9015
|
+
}
|
|
9016
|
+
function reshapeHit(hit, includeContext) {
|
|
9017
|
+
const out = { ...hit };
|
|
9018
|
+
if ("metadata" in out) out.metadata = trimMetadata(out.metadata);
|
|
9019
|
+
const chunk = typeof hit.chunkText === "string" ? hit.chunkText : void 0;
|
|
9020
|
+
const ctx = typeof hit.contextText === "string" ? hit.contextText : void 0;
|
|
9021
|
+
if (includeContext) {
|
|
9022
|
+
if (chunk && ctx && ctx.includes(chunk)) delete out.chunkText;
|
|
9023
|
+
} else {
|
|
9024
|
+
delete out.contextText;
|
|
9025
|
+
}
|
|
9026
|
+
if ((out.snippet == null || out.snippet === "") && chunk) {
|
|
9027
|
+
out.snippet = chunk.length > SNIPPET_MAX_CHARS ? chunk.slice(0, SNIPPET_MAX_CHARS) + "\u2026" : chunk;
|
|
9028
|
+
}
|
|
9029
|
+
return out;
|
|
9030
|
+
}
|
|
8997
9031
|
var inputSchema = {
|
|
8998
9032
|
query: z.string().min(1, "query must be non-empty").describe("Natural-language or keyword query."),
|
|
8999
9033
|
mode: z.enum(["HYBRID", "TEXT", "SEMANTIC"]).optional().describe("HYBRID (BM25 + dense, default), TEXT (BM25 only), or SEMANTIC (dense only)."),
|
|
@@ -9008,7 +9042,9 @@ var inputSchema = {
|
|
|
9008
9042
|
rootFolderId: z.string().optional().describe("Scope to content under this folder AND all its descendants (subtree-root folder id)."),
|
|
9009
9043
|
// Content narrowing.
|
|
9010
9044
|
contentTypes: z.array(z.enum(["documents", "records"])).optional().describe('Narrow to content types. ["documents"] or ["records"]; omit for unified (both).'),
|
|
9011
|
-
typeName: z.string().optional().describe(
|
|
9045
|
+
typeName: z.string().optional().describe(
|
|
9046
|
+
'Restrict hits to this schema type (e.g. "patient", "runbook"). Applies to BOTH documents and records \u2014 combine with contentTypes to scope within one content type. Untyped content never matches.'
|
|
9047
|
+
),
|
|
9012
9048
|
filters: z.record(z.unknown()).optional().describe(
|
|
9013
9049
|
'Field-level metadata filters (AND-combined). Value = scalar (equality), array (OR-set), or operator map: $eq/$ne/$gt/$gte/$lt/$lte (scalar) or $in/$nin (array). e.g. {"status":"open"} or {"price":{"$gte":100,"$lte":500}}.'
|
|
9014
9050
|
),
|
|
@@ -9023,6 +9059,9 @@ var inputSchema = {
|
|
|
9023
9059
|
minSimilarity: z.number().min(0).max(1).optional().describe("Minimum semantic similarity (0.0\u20131.0); results below are excluded."),
|
|
9024
9060
|
minTextRelevance: z.number().min(0).max(1).optional().describe("Minimum relative keyword (BM25) relevance (0.0\u20131.0); the keyword-leg analogue of minSimilarity."),
|
|
9025
9061
|
uniqueDocuments: z.boolean().optional().describe("When true, return at most one chunk per source document."),
|
|
9062
|
+
includeContext: z.boolean().optional().describe(
|
|
9063
|
+
"Include the broader passage (contextText) around each matched chunk. Off by default to keep the result compact \u2014 each hit returns its matched chunkText plus a short snippet. Turn this on only when you need more surrounding context inline; for a full deep read, fetch the source with document_get."
|
|
9064
|
+
),
|
|
9026
9065
|
requireComplete: z.boolean().optional().describe(
|
|
9027
9066
|
"When true, fail closed (HTTP 503) if a search backend is unavailable rather than silently returning partial/degraded results. Default false (degrade to the surviving engine; the result still flags degraded)."
|
|
9028
9067
|
)
|
|
@@ -9030,7 +9069,7 @@ var inputSchema = {
|
|
|
9030
9069
|
var hybridSearch = ({ client, log }) => ({
|
|
9031
9070
|
name: "hybrid_search",
|
|
9032
9071
|
title: "Hybrid search",
|
|
9033
|
-
description:
|
|
9072
|
+
description: `Search the partner tenant's indexed content (documents + structured records) using hybrid BM25 + dense ranking. Returns up to 10 results (default 3) with citations and surrounding context for grounding follow-up reasoning. Narrow with contentTypes/typeName/filters, ownership (userId/orgId/clientId), folder scope, a created date window, keyword precision (textMode OR/AND/PHRASE), relevance floors (minSimilarity/minTextRelevance), uniqueDocuments, and requireComplete (fail closed on a degraded backend). Tenant-isolated; the caller's scoped key fully constrains which content is visible. ARCHIVED (soft-retracted) documents never appear in results \u2014 fetch them by id via document_get, or restore them via document_update. To stay within the agent context window each hit returns its matched chunkText + a short snippet by default; set includeContext:true for the broader surrounding passage. If the whole result would be too large it is truncated to the top hits (truncated:true). textLegEmpty:true flags that the keyword leg matched nothing (all textScores 0) \u2014 usually a too-long PHRASE query; retry with textMode:"OR".`,
|
|
9034
9073
|
inputSchema,
|
|
9035
9074
|
handler: async (args) => {
|
|
9036
9075
|
const limit = args.limit ?? MCP_DEFAULT_LIMIT;
|
|
@@ -9057,13 +9096,30 @@ var hybridSearch = ({ client, log }) => ({
|
|
|
9057
9096
|
uniqueDocuments: args.uniqueDocuments,
|
|
9058
9097
|
requireComplete: args.requireComplete
|
|
9059
9098
|
});
|
|
9099
|
+
const hits = Array.isArray(
|
|
9100
|
+
result.results
|
|
9101
|
+
) ? result.results : [];
|
|
9102
|
+
const includeContext = args.includeContext ?? false;
|
|
9103
|
+
let shaped = hits.map((h) => reshapeHit(h, includeContext));
|
|
9104
|
+
const textLegEmpty = mode !== "SEMANTIC" && hits.length > 0 && hits.every((h) => !(Number(h.textScore) > 0));
|
|
9105
|
+
const envelope = {
|
|
9106
|
+
...result,
|
|
9107
|
+
results: shaped
|
|
9108
|
+
};
|
|
9109
|
+
if (textLegEmpty) envelope.textLegEmpty = true;
|
|
9110
|
+
let text = JSON.stringify(envelope, null, 2);
|
|
9111
|
+
while (shaped.length > 1 && text.length > MCP_RESPONSE_CHAR_BUDGET) {
|
|
9112
|
+
shaped = shaped.slice(0, -1);
|
|
9113
|
+
envelope.results = shaped;
|
|
9114
|
+
envelope.truncated = true;
|
|
9115
|
+
envelope.returnedResults = shaped.length;
|
|
9116
|
+
text = JSON.stringify(envelope, null, 2);
|
|
9117
|
+
}
|
|
9060
9118
|
log.debug(
|
|
9061
|
-
{ tool: "hybrid_search", mode, limit, returned:
|
|
9119
|
+
{ tool: "hybrid_search", mode, limit, returned: shaped.length, textLegEmpty },
|
|
9062
9120
|
"hybrid_search ok"
|
|
9063
9121
|
);
|
|
9064
|
-
return {
|
|
9065
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
9066
|
-
};
|
|
9122
|
+
return { content: [{ type: "text", text }] };
|
|
9067
9123
|
} catch (err) {
|
|
9068
9124
|
log.warn({ tool: "hybrid_search", err: String(err) }, "hybrid_search failed");
|
|
9069
9125
|
return toolError("hybrid_search", err);
|
|
@@ -9098,7 +9154,9 @@ var MCP_MAX_LIMIT2 = 10;
|
|
|
9098
9154
|
var inputSchema2 = {
|
|
9099
9155
|
type: z2.string().min(1, "type (record type) is required").describe('Record type / schema name (e.g. "patient", "clinical_note").'),
|
|
9100
9156
|
// Lookup-mode args — provide `field` plus EXACTLY ONE of: value | from+to | prefix.
|
|
9101
|
-
field: z2.string().optional().describe(
|
|
9157
|
+
field: z2.string().optional().describe(
|
|
9158
|
+
'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.)'
|
|
9159
|
+
),
|
|
9102
9160
|
value: z2.string().optional().describe("Lookup mode (equality): exact-match value for `field`."),
|
|
9103
9161
|
from: z2.string().optional().describe("Lookup mode (range): inclusive lower bound; requires `to`. Non-sensitive fields only."),
|
|
9104
9162
|
to: z2.string().optional().describe("Lookup mode (range): inclusive upper bound; requires `from`."),
|
|
@@ -9193,19 +9251,77 @@ var record_query_default = recordQuery;
|
|
|
9193
9251
|
|
|
9194
9252
|
// src/tools/record_get.ts
|
|
9195
9253
|
import { z as z3 } from "zod";
|
|
9254
|
+
|
|
9255
|
+
// src/tools/resolve-external-id.ts
|
|
9256
|
+
function validateSelector(sel, entity) {
|
|
9257
|
+
const hasId = sel.id !== void 0 && sel.id !== "";
|
|
9258
|
+
const hasExt = sel.externalId !== void 0 && sel.externalId !== "";
|
|
9259
|
+
if (hasId && hasExt) {
|
|
9260
|
+
return `Provide either id or externalId (not both) to identify the ${entity}.`;
|
|
9261
|
+
}
|
|
9262
|
+
if (!hasId && !hasExt) {
|
|
9263
|
+
return `Provide id, or externalId + type, to identify the ${entity}.`;
|
|
9264
|
+
}
|
|
9265
|
+
if (hasExt && (sel.type === void 0 || sel.type === "")) {
|
|
9266
|
+
return "type is required when selecting by externalId (externalId is unique within a type).";
|
|
9267
|
+
}
|
|
9268
|
+
return void 0;
|
|
9269
|
+
}
|
|
9270
|
+
async function resolveRecordIdByExternalId(client, sel) {
|
|
9271
|
+
const err = validateSelector(sel, "record");
|
|
9272
|
+
if (err) return { error: err };
|
|
9273
|
+
if (sel.id) return { id: sel.id };
|
|
9274
|
+
const page = await client.records.lookupRecordsByBody({
|
|
9275
|
+
type: sel.type,
|
|
9276
|
+
field: "externalId",
|
|
9277
|
+
value: sel.externalId,
|
|
9278
|
+
limit: 1
|
|
9279
|
+
});
|
|
9280
|
+
const items = pageItems(page);
|
|
9281
|
+
if (items.length === 0) {
|
|
9282
|
+
return { error: `No record of type '${sel.type}' with externalId '${sel.externalId}'.` };
|
|
9283
|
+
}
|
|
9284
|
+
return { id: items[0].id };
|
|
9285
|
+
}
|
|
9286
|
+
async function resolveDocumentIdByExternalId(client, sel) {
|
|
9287
|
+
const err = validateSelector(sel, "document");
|
|
9288
|
+
if (err) return { error: err };
|
|
9289
|
+
if (sel.id) return { id: sel.id };
|
|
9290
|
+
const page = await client.documents.lookupDocumentsByBody({
|
|
9291
|
+
type: sel.type,
|
|
9292
|
+
field: "externalId",
|
|
9293
|
+
value: sel.externalId,
|
|
9294
|
+
limit: 1
|
|
9295
|
+
});
|
|
9296
|
+
const items = pageItems(page);
|
|
9297
|
+
if (items.length === 0) {
|
|
9298
|
+
return { error: `No document of type '${sel.type}' with externalId '${sel.externalId}'.` };
|
|
9299
|
+
}
|
|
9300
|
+
return { id: items[0].id };
|
|
9301
|
+
}
|
|
9302
|
+
|
|
9303
|
+
// src/tools/record_get.ts
|
|
9196
9304
|
var MAX_PAYLOAD_CHARS = 32e3;
|
|
9197
9305
|
var inputSchema3 = {
|
|
9198
|
-
id: z3.string().
|
|
9306
|
+
id: z3.string().optional().describe("The Vectros record id (e.g. from record_query or hybrid_search). Provide this OR externalId+type."),
|
|
9307
|
+
externalId: z3.string().optional().describe(
|
|
9308
|
+
"Your caller-owned externalId \u2014 an alternative selector to `id`. Requires `type` (externalId is unique within a type). Resolves to the record without a manual record_query lookup first."
|
|
9309
|
+
),
|
|
9310
|
+
type: z3.string().optional().describe("Record type \u2014 REQUIRED when selecting by `externalId` (externalId is unique within a type).")
|
|
9199
9311
|
};
|
|
9200
9312
|
var recordGet = ({ client, log }) => ({
|
|
9201
9313
|
name: "record_get",
|
|
9202
|
-
title: "Get a record by id",
|
|
9203
|
-
description: "Fetch a single structured record
|
|
9314
|
+
title: "Get a record by id or externalId",
|
|
9315
|
+
description: "Fetch a single structured record, including the full payload. Select it EITHER by its Vectros `id` (e.g. from record_query / hybrid_search) OR by your own `externalId` + `type` \u2014 the same key you passed to record_create, resolved for you (no manual record_query lookup needed). Very large payloads are truncated to protect the agent context window.",
|
|
9204
9316
|
inputSchema: inputSchema3,
|
|
9205
9317
|
handler: async (args) => {
|
|
9206
9318
|
const id = args.id;
|
|
9319
|
+
const externalId = args.externalId;
|
|
9320
|
+
const type = args.type;
|
|
9207
9321
|
try {
|
|
9208
|
-
const
|
|
9322
|
+
const resolved = await resolveRecordIdByExternalId(client, { id, externalId, type });
|
|
9323
|
+
if ("error" in resolved) return toolError("record_get", new Error(resolved.error));
|
|
9324
|
+
const record = await client.records.getRecord({ id: resolved.id });
|
|
9209
9325
|
const out = { ...record };
|
|
9210
9326
|
if (record.payload !== void 0) {
|
|
9211
9327
|
const json = JSON.stringify(record.payload);
|
|
@@ -9217,12 +9333,12 @@ var recordGet = ({ client, log }) => ({
|
|
|
9217
9333
|
}
|
|
9218
9334
|
}
|
|
9219
9335
|
log.debug(
|
|
9220
|
-
{ tool: "record_get", id, truncated: out.payloadTruncated === true },
|
|
9336
|
+
{ tool: "record_get", id: resolved.id, externalId, truncated: out.payloadTruncated === true },
|
|
9221
9337
|
"record_get ok"
|
|
9222
9338
|
);
|
|
9223
9339
|
return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
|
|
9224
9340
|
} catch (err) {
|
|
9225
|
-
log.warn({ tool: "record_get", id, err: String(err) }, "record_get failed");
|
|
9341
|
+
log.warn({ tool: "record_get", id, externalId, type, err: String(err) }, "record_get failed");
|
|
9226
9342
|
return toolError("record_get", err);
|
|
9227
9343
|
}
|
|
9228
9344
|
}
|
|
@@ -9282,7 +9398,11 @@ var record_create_default = recordCreate;
|
|
|
9282
9398
|
// src/tools/record_update.ts
|
|
9283
9399
|
import { z as z5 } from "zod";
|
|
9284
9400
|
var inputSchema5 = {
|
|
9285
|
-
id: z5.string().
|
|
9401
|
+
id: z5.string().optional().describe("The Vectros record id to update. Provide this OR externalId+type."),
|
|
9402
|
+
externalId: z5.string().optional().describe(
|
|
9403
|
+
"Your caller-owned externalId \u2014 an alternative selector to `id` (requires `type`). Updates the record you created under that externalId without a manual record_query lookup first."
|
|
9404
|
+
),
|
|
9405
|
+
type: z5.string().optional().describe("Record type \u2014 REQUIRED when selecting by `externalId` (externalId is unique within a type)."),
|
|
9286
9406
|
fields: z5.record(z5.string(), z5.unknown()).describe(
|
|
9287
9407
|
"Field\u2192value changes DEEP-MERGED into the existing payload: keys you send overwrite (recursing into nested objects), a key set to `null` is deleted, and unspecified fields are preserved."
|
|
9288
9408
|
),
|
|
@@ -9293,23 +9413,27 @@ var inputSchema5 = {
|
|
|
9293
9413
|
};
|
|
9294
9414
|
var recordUpdate = ({ client, log }) => ({
|
|
9295
9415
|
name: "record_update",
|
|
9296
|
-
title: "Update a record",
|
|
9297
|
-
description: "Update a record by id. The provided `fields` are DEEP-MERGED into the existing payload (unspecified fields are preserved; a field set to `null` is deleted). Pass `expectedVersion` (the version you last read) for safe concurrent edits \u2014 a stale update is refused (409). Use `status` to archive without deleting. Requires the key to allow records:u.",
|
|
9416
|
+
title: "Update a record by id or externalId",
|
|
9417
|
+
description: "Update a record \u2014 select it by its Vectros `id` OR by your own `externalId` + `type` (the same key you passed to record_create, resolved for you). The provided `fields` are DEEP-MERGED into the existing payload (unspecified fields are preserved; a field set to `null` is deleted). Pass `expectedVersion` (the version you last read) for safe concurrent edits \u2014 a stale update is refused (409). Use `status` to archive without deleting. Requires the key to allow records:u.",
|
|
9298
9418
|
inputSchema: inputSchema5,
|
|
9299
9419
|
handler: async (args) => {
|
|
9300
9420
|
const id = args.id;
|
|
9421
|
+
const externalId = args.externalId;
|
|
9422
|
+
const type = args.type;
|
|
9301
9423
|
const fields = args.fields;
|
|
9302
9424
|
const status = args.status;
|
|
9303
9425
|
const expectedVersion = args.expectedVersion;
|
|
9304
9426
|
try {
|
|
9427
|
+
const resolved = await resolveRecordIdByExternalId(client, { id, externalId, type });
|
|
9428
|
+
if ("error" in resolved) return toolError("record_update", new Error(resolved.error));
|
|
9305
9429
|
const body = { payload: fields };
|
|
9306
9430
|
if (status !== void 0) body.status = status;
|
|
9307
9431
|
if (expectedVersion !== void 0) body.expectedVersion = expectedVersion;
|
|
9308
|
-
const updated = await client.records.patchRecord({ id, body });
|
|
9309
|
-
log.debug({ tool: "record_update", id }, "record_update ok");
|
|
9432
|
+
const updated = await client.records.patchRecord({ id: resolved.id, body });
|
|
9433
|
+
log.debug({ tool: "record_update", id: resolved.id, externalId }, "record_update ok");
|
|
9310
9434
|
return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
|
|
9311
9435
|
} catch (err) {
|
|
9312
|
-
log.warn({ tool: "record_update", id, err: String(err) }, "record_update failed");
|
|
9436
|
+
log.warn({ tool: "record_update", id, externalId, type, err: String(err) }, "record_update failed");
|
|
9313
9437
|
return toolError("record_update", err);
|
|
9314
9438
|
}
|
|
9315
9439
|
}
|
|
@@ -9576,7 +9700,11 @@ var list_schemas_default = listSchemas;
|
|
|
9576
9700
|
import { z as z10 } from "zod";
|
|
9577
9701
|
var MAX_TEXT_CHARS = 32e3;
|
|
9578
9702
|
var inputSchema10 = {
|
|
9579
|
-
documentId: z10.string().
|
|
9703
|
+
documentId: z10.string().optional().describe("The document ID returned by document_ingest or hybrid_search. Provide this OR externalId+type."),
|
|
9704
|
+
externalId: z10.string().optional().describe(
|
|
9705
|
+
"Your caller-owned externalId \u2014 an alternative selector to `documentId` (requires `type`). Fetches the document you ingested under that externalId without a manual lookup first."
|
|
9706
|
+
),
|
|
9707
|
+
type: z10.string().optional().describe("Document type \u2014 REQUIRED when selecting by `externalId` (externalId is unique within a type)."),
|
|
9580
9708
|
includeText: z10.boolean().optional().describe(
|
|
9581
9709
|
"If true, fetch and include the document text (capped at ~8K tokens, truncated: true flag if cut). Default false \u2014 metadata only. For larger documents, prefer `document_ask` over dumping text into context."
|
|
9582
9710
|
),
|
|
@@ -9596,13 +9724,18 @@ function isNotAvailable(err) {
|
|
|
9596
9724
|
var documentGet = ({ client, log }) => ({
|
|
9597
9725
|
name: "document_get",
|
|
9598
9726
|
title: "Get document metadata (and optionally text)",
|
|
9599
|
-
description: "Fetch metadata for a single document by
|
|
9727
|
+
description: "Fetch metadata for a single document \u2014 select it by its Vectros `documentId` OR by your own `externalId` + `type` (resolved for you). Pass `includeText: true` to also fetch the text body (capped at ~8K tokens; `truncated: true` flag if cut). Text is available for every text-ingested document and for file-ingested documents unless they were uploaded with `storeText: false` (extracted text discarded after indexing) \u2014 then `textAvailable: false` is set; the original file is still retrievable via `includeDownloadUrl`. Pass `includeDownloadUrl: true` to also get a short-lived presigned `downloadUrl` for the original file (file-backed documents only). Returns the full document metadata, including its lifecycle `status` (ACTIVE, or ARCHIVED when soft-retracted from search \u2014 restorable via document_update) and its processing `indexStatus`.",
|
|
9600
9728
|
inputSchema: inputSchema10,
|
|
9601
9729
|
handler: async (args) => {
|
|
9602
|
-
const
|
|
9730
|
+
const documentId = args.documentId;
|
|
9731
|
+
const externalId = args.externalId;
|
|
9732
|
+
const type = args.type;
|
|
9603
9733
|
const includeText = args.includeText ?? false;
|
|
9604
9734
|
const includeDownloadUrl = args.includeDownloadUrl ?? false;
|
|
9605
9735
|
try {
|
|
9736
|
+
const resolved = await resolveDocumentIdByExternalId(client, { id: documentId, externalId, type });
|
|
9737
|
+
if ("error" in resolved) return toolError("document_get", new Error(resolved.error));
|
|
9738
|
+
const id = resolved.id;
|
|
9606
9739
|
const doc = await client.documents.getDocument({ id });
|
|
9607
9740
|
let extra = {};
|
|
9608
9741
|
if (includeText) {
|
|
@@ -9615,7 +9748,7 @@ var documentGet = ({ client, log }) => ({
|
|
|
9615
9748
|
if (isNotAvailable(textErr)) {
|
|
9616
9749
|
log.debug(
|
|
9617
9750
|
{ tool: "document_get", id },
|
|
9618
|
-
"document_get text unavailable (
|
|
9751
|
+
"document_get text unavailable (not retained \u2014 storeText:false upload)"
|
|
9619
9752
|
);
|
|
9620
9753
|
extra = { ...extra, textAvailable: false };
|
|
9621
9754
|
} else {
|
|
@@ -9649,7 +9782,7 @@ var documentGet = ({ client, log }) => ({
|
|
|
9649
9782
|
]
|
|
9650
9783
|
};
|
|
9651
9784
|
} catch (err) {
|
|
9652
|
-
log.warn({ tool: "document_get",
|
|
9785
|
+
log.warn({ tool: "document_get", documentId, externalId, type, err: String(err) }, "document_get failed");
|
|
9653
9786
|
return toolError("document_get", err);
|
|
9654
9787
|
}
|
|
9655
9788
|
}
|
|
@@ -9764,13 +9897,22 @@ var document_query_default = documentQuery;
|
|
|
9764
9897
|
// src/tools/document_update.ts
|
|
9765
9898
|
import { z as z12 } from "zod";
|
|
9766
9899
|
var inputSchema12 = {
|
|
9767
|
-
documentId: z12.string().
|
|
9900
|
+
documentId: z12.string().optional().describe("The Vectros document id to update. Provide this OR externalId+type."),
|
|
9901
|
+
externalId: z12.string().optional().describe(
|
|
9902
|
+
"Your caller-owned externalId \u2014 an alternative selector to `documentId` (requires `type`). Updates the document you ingested under that externalId without a manual lookup first."
|
|
9903
|
+
),
|
|
9904
|
+
type: z12.string().optional().describe("Document type \u2014 REQUIRED when selecting by `externalId` (externalId is unique within a type)."),
|
|
9768
9905
|
title: z12.string().optional().describe("New title. Omit to keep the current title."),
|
|
9769
9906
|
fields: z12.record(z12.string(), z12.unknown()).optional().describe(
|
|
9770
9907
|
"Typed-payload field\u2192value changes DEEP-MERGED into the existing payload (a field set to `null` is deleted; unspecified fields preserved). Omit to leave the payload unchanged."
|
|
9771
9908
|
),
|
|
9909
|
+
text: z12.string().optional().describe(
|
|
9910
|
+
"New raw text body. Supplying it re-ingests the document body and re-queues it for indexing, so the search index reflects the edit (text-ingested documents only \u2014 not file-backed ones). Omit to leave the stored text unchanged. This is the in-place alternative to re-ingesting with upsert:true."
|
|
9911
|
+
),
|
|
9772
9912
|
folderId: z12.string().optional().describe("Move the document into this folder (cannot be cleared once set)."),
|
|
9773
|
-
|
|
9913
|
+
status: z12.enum(["ACTIVE", "ARCHIVED"]).optional().describe(
|
|
9914
|
+
"Lifecycle status. `ARCHIVED` soft-retracts the document \u2014 it is pulled from search and recall but kept and recoverable; set it back to `ACTIVE` to re-index and restore it. The way to retire superseded content when the credential has no delete authority. Omit to leave unchanged."
|
|
9915
|
+
),
|
|
9774
9916
|
userId: z12.string().optional().describe("Reassign the owning user (Vectros UUID)."),
|
|
9775
9917
|
orgId: z12.string().optional().describe("Reassign the owning org (Vectros UUID)."),
|
|
9776
9918
|
clientId: z12.string().optional().describe("Reassign the associated client (Vectros UUID)."),
|
|
@@ -9781,33 +9923,39 @@ var inputSchema12 = {
|
|
|
9781
9923
|
var documentUpdate = ({ client, log }) => ({
|
|
9782
9924
|
name: "document_update",
|
|
9783
9925
|
title: "Update a document",
|
|
9784
|
-
description:
|
|
9926
|
+
description: 'Update a document \u2014 select it by its Vectros `documentId` OR by your own `externalId` + `type` (resolved for you). `fields` are DEEP-MERGED into the existing typed payload (unspecified fields preserved; a field set to `null` is deleted); title/folderId/status/ownership are updated when provided and preserved otherwise. Supply `text` to re-ingest and re-index the document body in place (text documents only). Set `status: "ARCHIVED"` to soft-retract a document (pulled from search, kept and recoverable) and `status: "ACTIVE"` to restore it. Pass `expectedVersion` (the version you last read) for safe concurrent edits \u2014 a stale update is refused (409). Requires the key to allow documents:u.',
|
|
9785
9927
|
inputSchema: inputSchema12,
|
|
9786
9928
|
handler: async (args) => {
|
|
9787
9929
|
const documentId = args.documentId;
|
|
9930
|
+
const externalId = args.externalId;
|
|
9931
|
+
const type = args.type;
|
|
9788
9932
|
const title = args.title;
|
|
9789
9933
|
const fields = args.fields;
|
|
9934
|
+
const text = args.text;
|
|
9790
9935
|
const folderId = args.folderId;
|
|
9791
|
-
const
|
|
9936
|
+
const status = args.status;
|
|
9792
9937
|
const userId = args.userId;
|
|
9793
9938
|
const orgId = args.orgId;
|
|
9794
9939
|
const clientId = args.clientId;
|
|
9795
9940
|
const expectedVersion = args.expectedVersion;
|
|
9796
9941
|
try {
|
|
9942
|
+
const resolved = await resolveDocumentIdByExternalId(client, { id: documentId, externalId, type });
|
|
9943
|
+
if ("error" in resolved) return toolError("document_update", new Error(resolved.error));
|
|
9797
9944
|
const body = {};
|
|
9798
9945
|
if (title !== void 0) body.title = title;
|
|
9799
9946
|
if (fields !== void 0) body.payload = fields;
|
|
9947
|
+
if (text !== void 0) body.text = text;
|
|
9800
9948
|
if (folderId !== void 0) body.folderId = folderId;
|
|
9801
|
-
if (
|
|
9949
|
+
if (status !== void 0) body.status = status;
|
|
9802
9950
|
if (userId !== void 0) body.userId = userId;
|
|
9803
9951
|
if (orgId !== void 0) body.orgId = orgId;
|
|
9804
9952
|
if (clientId !== void 0) body.clientId = clientId;
|
|
9805
9953
|
if (expectedVersion !== void 0) body.expectedVersion = expectedVersion;
|
|
9806
9954
|
const updated = await client.documents.patchDocument({
|
|
9807
|
-
id:
|
|
9955
|
+
id: resolved.id,
|
|
9808
9956
|
body
|
|
9809
9957
|
});
|
|
9810
|
-
log.debug({ tool: "document_update", documentId }, "document_update ok");
|
|
9958
|
+
log.debug({ tool: "document_update", documentId: resolved.id, externalId }, "document_update ok");
|
|
9811
9959
|
return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
|
|
9812
9960
|
} catch (err) {
|
|
9813
9961
|
log.warn({ tool: "document_update", documentId, err: String(err) }, "document_update failed");
|
|
@@ -9825,7 +9973,7 @@ var inputSchema13 = {
|
|
|
9825
9973
|
var documentDelete = ({ client, log }) => ({
|
|
9826
9974
|
name: "document_delete",
|
|
9827
9975
|
title: "Delete a document",
|
|
9828
|
-
description:
|
|
9976
|
+
description: 'Permanently delete a document by id (removes it and its indexed content). Requires the key to allow documents:d \u2014 a key without it gets a permission error. To archive without deleting, use document_update to set status: "ARCHIVED" instead (recoverable, and needs no delete authority).',
|
|
9829
9977
|
inputSchema: inputSchema13,
|
|
9830
9978
|
handler: async (args) => {
|
|
9831
9979
|
const documentId = args.documentId;
|
|
@@ -10105,6 +10253,9 @@ var inputSchema19 = {
|
|
|
10105
10253
|
externalId: z18.string().optional().describe(
|
|
10106
10254
|
"Stable caller-supplied id. Immutable; unique per type within your context. Re-ingesting with the same externalId returns the existing document (idempotent) instead of creating a duplicate \u2014 use it to make ingests safely retryable and as the key other records reference. Mirrors record_create."
|
|
10107
10255
|
),
|
|
10256
|
+
upsert: z18.boolean().optional().describe(
|
|
10257
|
+
"When true, re-ingesting an existing externalId OVERWRITES that document and re-indexes the new body (text mode) / re-uploads and re-indexes the new file (file mode), instead of returning the existing one unchanged. This is the curation/re-sync primitive: edit a source, re-ingest with upsert:true, and the search index reflects the new content. Requires the documents:u scope. IMPORTANT: pass the SAME schemaId (and type) you used originally \u2014 externalId is unique WITHIN a type, so omitting schemaId resolves in the untyped namespace and mints a duplicate instead of updating. The response `created` flag confirms which happened (created:true = a new document was minted)."
|
|
10258
|
+
),
|
|
10108
10259
|
schemaId: z18.string().optional().describe(
|
|
10109
10260
|
"Bind this document to a record schema. When set, `payload` is validated against the schema and its lookup fields become directly queryable via document_query (records parity). Resolve a schema id from its type via list_schemas. Omit for an untyped document."
|
|
10110
10261
|
),
|
|
@@ -10114,7 +10265,7 @@ var inputSchema19 = {
|
|
|
10114
10265
|
// Inline text mode (mutually exclusive with filePath):
|
|
10115
10266
|
text: z18.string().min(1).optional().describe("Text body to ingest inline. Mutually exclusive with filePath. Either text or filePath is required."),
|
|
10116
10267
|
storeText: z18.boolean().optional().describe(
|
|
10117
|
-
"
|
|
10268
|
+
"Whether the document's text is retained after indexing (default true). Applies to BOTH modes and is fixed at ingest time. Text mode: the ingested body is always retained (this flag is effectively always true \u2014 the body is the document). File mode: true keeps the extracted text retrievable via document_get(includeText:true) and usable by document_ask; false discards the extracted text once indexing completes \u2014 search still works and the original file remains downloadable, but includeText returns textAvailable:false and document_ask is unavailable."
|
|
10118
10269
|
),
|
|
10119
10270
|
// File upload mode (mutually exclusive with text; stdio-transport-only):
|
|
10120
10271
|
filePath: z18.string().min(1).optional().describe(
|
|
@@ -10130,13 +10281,14 @@ var inputSchema19 = {
|
|
|
10130
10281
|
var documentIngest = ({ client, log, transport, ingestRoot }) => ({
|
|
10131
10282
|
name: "document_ingest",
|
|
10132
10283
|
title: "Ingest a new document (text body or file upload)",
|
|
10133
|
-
description: "Create a new document in
|
|
10284
|
+
description: "Create a new document in your Vectros tenant. Two modes:\n \u2022 Text mode: pass `text` (string body). Single-call. Use for crawled content, notes, generated text.\n \u2022 File mode: pass `filePath` (local path on the MCP server's host machine). STDIO-TRANSPORT ONLY \u2014 rejected on HTTP transport. MCP server reads the bytes, requests a presigned upload URL, and PUTs them. Returns when the upload is accepted (indexStatus: PENDING_INDEX); poll with `document_get` until indexStatus is INDEXED.\nEither `text` or `filePath` must be present; both is an error. Idempotent by `externalId` (re-ingest returns the existing document, not a duplicate). To UPDATE an existing document instead \u2014 re-index an edited body \u2014 pass `upsert:true` with the same `externalId` (and the same `schemaId`); this is the re-sync primitive for keeping a knowledge base current. Pass `schemaId` + `payload` for a typed, lookup-queryable document (records parity). indexMode defaults to HYBRID for untyped documents (omit to inherit a bound schema's default). storeText defaults to true and is fixed at ingest: in file mode, false discards the extracted text after indexing (search + file download keep working; text retrieval and document_ask do not). Text-mode bodies are always retained.",
|
|
10134
10285
|
inputSchema: inputSchema19,
|
|
10135
10286
|
handler: async (args) => {
|
|
10136
10287
|
const title = args.title;
|
|
10137
10288
|
const text = args.text;
|
|
10138
10289
|
const filePath = args.filePath;
|
|
10139
10290
|
const schemaId = args.schemaId;
|
|
10291
|
+
const upsert = args.upsert;
|
|
10140
10292
|
const indexMode = args.indexMode ?? (schemaId ? void 0 : "HYBRID");
|
|
10141
10293
|
if (!text && !filePath) {
|
|
10142
10294
|
return toolError(
|
|
@@ -10169,13 +10321,12 @@ var documentIngest = ({ client, log, transport, ingestRoot }) => ({
|
|
|
10169
10321
|
};
|
|
10170
10322
|
try {
|
|
10171
10323
|
if (text) {
|
|
10172
|
-
const storeText = args.storeText ?? true;
|
|
10173
10324
|
const result = await client.documents.ingestDocument({
|
|
10325
|
+
upsert,
|
|
10174
10326
|
body: {
|
|
10175
10327
|
title,
|
|
10176
10328
|
text,
|
|
10177
10329
|
indexMode,
|
|
10178
|
-
storeText,
|
|
10179
10330
|
...common
|
|
10180
10331
|
}
|
|
10181
10332
|
});
|
|
@@ -10206,10 +10357,13 @@ var documentIngest = ({ client, log, transport, ingestRoot }) => ({
|
|
|
10206
10357
|
}
|
|
10207
10358
|
const fileName = basename(safePath);
|
|
10208
10359
|
const fileType = args.fileType ?? inferMimeType(safePath);
|
|
10360
|
+
const storeTextFile = args.storeText ?? true;
|
|
10209
10361
|
const upload = await client.documents.uploadDocument({
|
|
10362
|
+
upsert,
|
|
10210
10363
|
fileName,
|
|
10211
10364
|
fileType,
|
|
10212
10365
|
indexMode,
|
|
10366
|
+
storeText: storeTextFile,
|
|
10213
10367
|
...common
|
|
10214
10368
|
});
|
|
10215
10369
|
const uploadUrl = upload?.uploadUrl;
|
|
@@ -10245,8 +10399,8 @@ var documentIngest = ({ client, log, transport, ingestRoot }) => ({
|
|
|
10245
10399
|
text: JSON.stringify(
|
|
10246
10400
|
{
|
|
10247
10401
|
...upload,
|
|
10248
|
-
|
|
10249
|
-
_note: "File uploaded; indexing is asynchronous. Poll document_get(id) until
|
|
10402
|
+
indexStatus: "PENDING_INDEX",
|
|
10403
|
+
_note: "File uploaded; indexing is asynchronous. Poll document_get(id) until indexStatus is INDEXED."
|
|
10250
10404
|
},
|
|
10251
10405
|
null,
|
|
10252
10406
|
2
|
|
@@ -10543,6 +10697,14 @@ function zodTypeToJsonSchema(schema) {
|
|
|
10543
10697
|
|
|
10544
10698
|
// src/server.ts
|
|
10545
10699
|
var DEFAULT_ENVIRONMENT = "https://api.vectros.ai";
|
|
10700
|
+
function formatArgError(tool, error) {
|
|
10701
|
+
const unknownKeys = error.issues.filter((i) => i.code === "unrecognized_keys").flatMap((i) => i.keys);
|
|
10702
|
+
if (unknownKeys.length > 0) {
|
|
10703
|
+
const valid = Object.keys(tool.inputSchema).join(", ");
|
|
10704
|
+
return `Unknown argument(s): ${unknownKeys.join(", ")}. Valid arguments for ${tool.name}: ${valid}. Pass only these \u2014 an unrecognized argument is rejected, not ignored.`;
|
|
10705
|
+
}
|
|
10706
|
+
return `Invalid arguments: ${error.message}`;
|
|
10707
|
+
}
|
|
10546
10708
|
var VectrosMCPServer = class {
|
|
10547
10709
|
server;
|
|
10548
10710
|
log;
|
|
@@ -10632,7 +10794,11 @@ var VectrosMCPServer = class {
|
|
|
10632
10794
|
name: t.name,
|
|
10633
10795
|
title: t.title,
|
|
10634
10796
|
description: t.description,
|
|
10635
|
-
|
|
10797
|
+
// additionalProperties:false advertises strict argument validation to the
|
|
10798
|
+
// MCP client (a well-behaved client rejects an unknown key before the call).
|
|
10799
|
+
// The server enforces it too (z.object(...).strict() below) so a raw JSON-RPC
|
|
10800
|
+
// call that skips client validation is still rejected — belt and suspenders.
|
|
10801
|
+
inputSchema: { ...zodShapeToJsonSchema(t.inputSchema), additionalProperties: false }
|
|
10636
10802
|
}))
|
|
10637
10803
|
})
|
|
10638
10804
|
);
|
|
@@ -10642,9 +10808,9 @@ var VectrosMCPServer = class {
|
|
|
10642
10808
|
if (!tool) {
|
|
10643
10809
|
result = toolError(req.params.name ?? "unknown", new Error("No such tool."));
|
|
10644
10810
|
} else {
|
|
10645
|
-
const parsed = z22.object(tool.inputSchema).safeParse(req.params.arguments ?? {});
|
|
10811
|
+
const parsed = z22.object(tool.inputSchema).strict().safeParse(req.params.arguments ?? {});
|
|
10646
10812
|
if (!parsed.success) {
|
|
10647
|
-
result = toolError(tool.name, new Error(
|
|
10813
|
+
result = toolError(tool.name, new Error(formatArgError(tool, parsed.error)));
|
|
10648
10814
|
} else {
|
|
10649
10815
|
try {
|
|
10650
10816
|
result = await tool.handler(
|