@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.js
CHANGED
|
@@ -504,10 +504,18 @@ var DocumentRequest;
|
|
|
504
504
|
Text: "TEXT",
|
|
505
505
|
None: "NONE"
|
|
506
506
|
};
|
|
507
|
+
DocumentRequest2.Status = {
|
|
508
|
+
Active: "ACTIVE",
|
|
509
|
+
Archived: "ARCHIVED"
|
|
510
|
+
};
|
|
507
511
|
})(DocumentRequest || (DocumentRequest = {}));
|
|
508
512
|
var DocumentResponse;
|
|
509
513
|
((DocumentResponse2) => {
|
|
510
514
|
DocumentResponse2.Status = {
|
|
515
|
+
Active: "ACTIVE",
|
|
516
|
+
Archived: "ARCHIVED"
|
|
517
|
+
};
|
|
518
|
+
DocumentResponse2.IndexStatus = {
|
|
511
519
|
PendingUpload: "PENDING_UPLOAD",
|
|
512
520
|
Uploaded: "UPLOADED",
|
|
513
521
|
Extracting: "EXTRACTING",
|
|
@@ -4913,7 +4921,7 @@ var DocumentsClient = class {
|
|
|
4913
4921
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/documents/{id}");
|
|
4914
4922
|
}
|
|
4915
4923
|
/**
|
|
4916
|
-
* 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`, `
|
|
4924
|
+
* 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.
|
|
4917
4925
|
*
|
|
4918
4926
|
* @param {Vectros.PatchDocumentRequest} request
|
|
4919
4927
|
* @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5165,7 +5173,7 @@ var DocumentsClient = class {
|
|
|
5165
5173
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/documents/{id}/download");
|
|
5166
5174
|
}
|
|
5167
5175
|
/**
|
|
5168
|
-
* Returns the full
|
|
5176
|
+
* 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.
|
|
5169
5177
|
*
|
|
5170
5178
|
* @param {Vectros.GetDocumentTextRequest} request
|
|
5171
5179
|
* @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5278,7 +5286,7 @@ var DocumentsClient = class {
|
|
|
5278
5286
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/documents/{id}/versions");
|
|
5279
5287
|
}
|
|
5280
5288
|
/**
|
|
5281
|
-
* 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
|
|
5289
|
+
* 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).
|
|
5282
5290
|
*
|
|
5283
5291
|
* @param {Vectros.FileUploadRequest} request
|
|
5284
5292
|
* @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8774,8 +8782,8 @@ function warnOnSuboptimalKey(key, log) {
|
|
|
8774
8782
|
|
|
8775
8783
|
// src/build-info.ts
|
|
8776
8784
|
var BUILD_INFO = {
|
|
8777
|
-
mcpServer: true ? "0.
|
|
8778
|
-
sdk: true ? "0.
|
|
8785
|
+
mcpServer: true ? "0.7.0" : "dev",
|
|
8786
|
+
sdk: true ? "0.33.0" : "dev"
|
|
8779
8787
|
};
|
|
8780
8788
|
|
|
8781
8789
|
// src/log.ts
|
|
@@ -9012,6 +9020,32 @@ ${detail}${pointer}`
|
|
|
9012
9020
|
// src/tools/hybrid_search.ts
|
|
9013
9021
|
var MCP_DEFAULT_LIMIT = 3;
|
|
9014
9022
|
var MCP_MAX_LIMIT = 10;
|
|
9023
|
+
var MCP_RESPONSE_CHAR_BUDGET = 24e3;
|
|
9024
|
+
var SNIPPET_MAX_CHARS = 160;
|
|
9025
|
+
var INTERNAL_METADATA_KEYS = /* @__PURE__ */ new Set(["tenantId", "owner_id", "model_type", "rootFolderId", "folderId"]);
|
|
9026
|
+
function trimMetadata(md) {
|
|
9027
|
+
if (!md || typeof md !== "object" || Array.isArray(md)) return md;
|
|
9028
|
+
const out = {};
|
|
9029
|
+
for (const [k, v] of Object.entries(md)) {
|
|
9030
|
+
if (!INTERNAL_METADATA_KEYS.has(k)) out[k] = v;
|
|
9031
|
+
}
|
|
9032
|
+
return out;
|
|
9033
|
+
}
|
|
9034
|
+
function reshapeHit(hit, includeContext) {
|
|
9035
|
+
const out = { ...hit };
|
|
9036
|
+
if ("metadata" in out) out.metadata = trimMetadata(out.metadata);
|
|
9037
|
+
const chunk = typeof hit.chunkText === "string" ? hit.chunkText : void 0;
|
|
9038
|
+
const ctx = typeof hit.contextText === "string" ? hit.contextText : void 0;
|
|
9039
|
+
if (includeContext) {
|
|
9040
|
+
if (chunk && ctx && ctx.includes(chunk)) delete out.chunkText;
|
|
9041
|
+
} else {
|
|
9042
|
+
delete out.contextText;
|
|
9043
|
+
}
|
|
9044
|
+
if ((out.snippet == null || out.snippet === "") && chunk) {
|
|
9045
|
+
out.snippet = chunk.length > SNIPPET_MAX_CHARS ? chunk.slice(0, SNIPPET_MAX_CHARS) + "\u2026" : chunk;
|
|
9046
|
+
}
|
|
9047
|
+
return out;
|
|
9048
|
+
}
|
|
9015
9049
|
var inputSchema = {
|
|
9016
9050
|
query: import_zod.z.string().min(1, "query must be non-empty").describe("Natural-language or keyword query."),
|
|
9017
9051
|
mode: import_zod.z.enum(["HYBRID", "TEXT", "SEMANTIC"]).optional().describe("HYBRID (BM25 + dense, default), TEXT (BM25 only), or SEMANTIC (dense only)."),
|
|
@@ -9026,7 +9060,9 @@ var inputSchema = {
|
|
|
9026
9060
|
rootFolderId: import_zod.z.string().optional().describe("Scope to content under this folder AND all its descendants (subtree-root folder id)."),
|
|
9027
9061
|
// Content narrowing.
|
|
9028
9062
|
contentTypes: import_zod.z.array(import_zod.z.enum(["documents", "records"])).optional().describe('Narrow to content types. ["documents"] or ["records"]; omit for unified (both).'),
|
|
9029
|
-
typeName: import_zod.z.string().optional().describe(
|
|
9063
|
+
typeName: import_zod.z.string().optional().describe(
|
|
9064
|
+
'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.'
|
|
9065
|
+
),
|
|
9030
9066
|
filters: import_zod.z.record(import_zod.z.unknown()).optional().describe(
|
|
9031
9067
|
'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}}.'
|
|
9032
9068
|
),
|
|
@@ -9041,6 +9077,9 @@ var inputSchema = {
|
|
|
9041
9077
|
minSimilarity: import_zod.z.number().min(0).max(1).optional().describe("Minimum semantic similarity (0.0\u20131.0); results below are excluded."),
|
|
9042
9078
|
minTextRelevance: import_zod.z.number().min(0).max(1).optional().describe("Minimum relative keyword (BM25) relevance (0.0\u20131.0); the keyword-leg analogue of minSimilarity."),
|
|
9043
9079
|
uniqueDocuments: import_zod.z.boolean().optional().describe("When true, return at most one chunk per source document."),
|
|
9080
|
+
includeContext: import_zod.z.boolean().optional().describe(
|
|
9081
|
+
"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."
|
|
9082
|
+
),
|
|
9044
9083
|
requireComplete: import_zod.z.boolean().optional().describe(
|
|
9045
9084
|
"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)."
|
|
9046
9085
|
)
|
|
@@ -9048,7 +9087,7 @@ var inputSchema = {
|
|
|
9048
9087
|
var hybridSearch = ({ client, log }) => ({
|
|
9049
9088
|
name: "hybrid_search",
|
|
9050
9089
|
title: "Hybrid search",
|
|
9051
|
-
description:
|
|
9090
|
+
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".`,
|
|
9052
9091
|
inputSchema,
|
|
9053
9092
|
handler: async (args) => {
|
|
9054
9093
|
const limit = args.limit ?? MCP_DEFAULT_LIMIT;
|
|
@@ -9075,13 +9114,30 @@ var hybridSearch = ({ client, log }) => ({
|
|
|
9075
9114
|
uniqueDocuments: args.uniqueDocuments,
|
|
9076
9115
|
requireComplete: args.requireComplete
|
|
9077
9116
|
});
|
|
9117
|
+
const hits = Array.isArray(
|
|
9118
|
+
result.results
|
|
9119
|
+
) ? result.results : [];
|
|
9120
|
+
const includeContext = args.includeContext ?? false;
|
|
9121
|
+
let shaped = hits.map((h) => reshapeHit(h, includeContext));
|
|
9122
|
+
const textLegEmpty = mode !== "SEMANTIC" && hits.length > 0 && hits.every((h) => !(Number(h.textScore) > 0));
|
|
9123
|
+
const envelope = {
|
|
9124
|
+
...result,
|
|
9125
|
+
results: shaped
|
|
9126
|
+
};
|
|
9127
|
+
if (textLegEmpty) envelope.textLegEmpty = true;
|
|
9128
|
+
let text = JSON.stringify(envelope, null, 2);
|
|
9129
|
+
while (shaped.length > 1 && text.length > MCP_RESPONSE_CHAR_BUDGET) {
|
|
9130
|
+
shaped = shaped.slice(0, -1);
|
|
9131
|
+
envelope.results = shaped;
|
|
9132
|
+
envelope.truncated = true;
|
|
9133
|
+
envelope.returnedResults = shaped.length;
|
|
9134
|
+
text = JSON.stringify(envelope, null, 2);
|
|
9135
|
+
}
|
|
9078
9136
|
log.debug(
|
|
9079
|
-
{ tool: "hybrid_search", mode, limit, returned:
|
|
9137
|
+
{ tool: "hybrid_search", mode, limit, returned: shaped.length, textLegEmpty },
|
|
9080
9138
|
"hybrid_search ok"
|
|
9081
9139
|
);
|
|
9082
|
-
return {
|
|
9083
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
9084
|
-
};
|
|
9140
|
+
return { content: [{ type: "text", text }] };
|
|
9085
9141
|
} catch (err) {
|
|
9086
9142
|
log.warn({ tool: "hybrid_search", err: String(err) }, "hybrid_search failed");
|
|
9087
9143
|
return toolError("hybrid_search", err);
|
|
@@ -9116,7 +9172,9 @@ var MCP_MAX_LIMIT2 = 10;
|
|
|
9116
9172
|
var inputSchema2 = {
|
|
9117
9173
|
type: import_zod2.z.string().min(1, "type (record type) is required").describe('Record type / schema name (e.g. "patient", "clinical_note").'),
|
|
9118
9174
|
// Lookup-mode args — provide `field` plus EXACTLY ONE of: value | from+to | prefix.
|
|
9119
|
-
field: import_zod2.z.string().optional().describe(
|
|
9175
|
+
field: import_zod2.z.string().optional().describe(
|
|
9176
|
+
'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.)'
|
|
9177
|
+
),
|
|
9120
9178
|
value: import_zod2.z.string().optional().describe("Lookup mode (equality): exact-match value for `field`."),
|
|
9121
9179
|
from: import_zod2.z.string().optional().describe("Lookup mode (range): inclusive lower bound; requires `to`. Non-sensitive fields only."),
|
|
9122
9180
|
to: import_zod2.z.string().optional().describe("Lookup mode (range): inclusive upper bound; requires `from`."),
|
|
@@ -9211,19 +9269,77 @@ var record_query_default = recordQuery;
|
|
|
9211
9269
|
|
|
9212
9270
|
// src/tools/record_get.ts
|
|
9213
9271
|
var import_zod3 = require("zod");
|
|
9272
|
+
|
|
9273
|
+
// src/tools/resolve-external-id.ts
|
|
9274
|
+
function validateSelector(sel, entity) {
|
|
9275
|
+
const hasId = sel.id !== void 0 && sel.id !== "";
|
|
9276
|
+
const hasExt = sel.externalId !== void 0 && sel.externalId !== "";
|
|
9277
|
+
if (hasId && hasExt) {
|
|
9278
|
+
return `Provide either id or externalId (not both) to identify the ${entity}.`;
|
|
9279
|
+
}
|
|
9280
|
+
if (!hasId && !hasExt) {
|
|
9281
|
+
return `Provide id, or externalId + type, to identify the ${entity}.`;
|
|
9282
|
+
}
|
|
9283
|
+
if (hasExt && (sel.type === void 0 || sel.type === "")) {
|
|
9284
|
+
return "type is required when selecting by externalId (externalId is unique within a type).";
|
|
9285
|
+
}
|
|
9286
|
+
return void 0;
|
|
9287
|
+
}
|
|
9288
|
+
async function resolveRecordIdByExternalId(client, sel) {
|
|
9289
|
+
const err = validateSelector(sel, "record");
|
|
9290
|
+
if (err) return { error: err };
|
|
9291
|
+
if (sel.id) return { id: sel.id };
|
|
9292
|
+
const page = await client.records.lookupRecordsByBody({
|
|
9293
|
+
type: sel.type,
|
|
9294
|
+
field: "externalId",
|
|
9295
|
+
value: sel.externalId,
|
|
9296
|
+
limit: 1
|
|
9297
|
+
});
|
|
9298
|
+
const items = pageItems(page);
|
|
9299
|
+
if (items.length === 0) {
|
|
9300
|
+
return { error: `No record of type '${sel.type}' with externalId '${sel.externalId}'.` };
|
|
9301
|
+
}
|
|
9302
|
+
return { id: items[0].id };
|
|
9303
|
+
}
|
|
9304
|
+
async function resolveDocumentIdByExternalId(client, sel) {
|
|
9305
|
+
const err = validateSelector(sel, "document");
|
|
9306
|
+
if (err) return { error: err };
|
|
9307
|
+
if (sel.id) return { id: sel.id };
|
|
9308
|
+
const page = await client.documents.lookupDocumentsByBody({
|
|
9309
|
+
type: sel.type,
|
|
9310
|
+
field: "externalId",
|
|
9311
|
+
value: sel.externalId,
|
|
9312
|
+
limit: 1
|
|
9313
|
+
});
|
|
9314
|
+
const items = pageItems(page);
|
|
9315
|
+
if (items.length === 0) {
|
|
9316
|
+
return { error: `No document of type '${sel.type}' with externalId '${sel.externalId}'.` };
|
|
9317
|
+
}
|
|
9318
|
+
return { id: items[0].id };
|
|
9319
|
+
}
|
|
9320
|
+
|
|
9321
|
+
// src/tools/record_get.ts
|
|
9214
9322
|
var MAX_PAYLOAD_CHARS = 32e3;
|
|
9215
9323
|
var inputSchema3 = {
|
|
9216
|
-
id: import_zod3.z.string().
|
|
9324
|
+
id: import_zod3.z.string().optional().describe("The Vectros record id (e.g. from record_query or hybrid_search). Provide this OR externalId+type."),
|
|
9325
|
+
externalId: import_zod3.z.string().optional().describe(
|
|
9326
|
+
"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."
|
|
9327
|
+
),
|
|
9328
|
+
type: import_zod3.z.string().optional().describe("Record type \u2014 REQUIRED when selecting by `externalId` (externalId is unique within a type).")
|
|
9217
9329
|
};
|
|
9218
9330
|
var recordGet = ({ client, log }) => ({
|
|
9219
9331
|
name: "record_get",
|
|
9220
|
-
title: "Get a record by id",
|
|
9221
|
-
description: "Fetch a single structured record
|
|
9332
|
+
title: "Get a record by id or externalId",
|
|
9333
|
+
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.",
|
|
9222
9334
|
inputSchema: inputSchema3,
|
|
9223
9335
|
handler: async (args) => {
|
|
9224
9336
|
const id = args.id;
|
|
9337
|
+
const externalId = args.externalId;
|
|
9338
|
+
const type = args.type;
|
|
9225
9339
|
try {
|
|
9226
|
-
const
|
|
9340
|
+
const resolved = await resolveRecordIdByExternalId(client, { id, externalId, type });
|
|
9341
|
+
if ("error" in resolved) return toolError("record_get", new Error(resolved.error));
|
|
9342
|
+
const record = await client.records.getRecord({ id: resolved.id });
|
|
9227
9343
|
const out = { ...record };
|
|
9228
9344
|
if (record.payload !== void 0) {
|
|
9229
9345
|
const json = JSON.stringify(record.payload);
|
|
@@ -9235,12 +9351,12 @@ var recordGet = ({ client, log }) => ({
|
|
|
9235
9351
|
}
|
|
9236
9352
|
}
|
|
9237
9353
|
log.debug(
|
|
9238
|
-
{ tool: "record_get", id, truncated: out.payloadTruncated === true },
|
|
9354
|
+
{ tool: "record_get", id: resolved.id, externalId, truncated: out.payloadTruncated === true },
|
|
9239
9355
|
"record_get ok"
|
|
9240
9356
|
);
|
|
9241
9357
|
return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
|
|
9242
9358
|
} catch (err) {
|
|
9243
|
-
log.warn({ tool: "record_get", id, err: String(err) }, "record_get failed");
|
|
9359
|
+
log.warn({ tool: "record_get", id, externalId, type, err: String(err) }, "record_get failed");
|
|
9244
9360
|
return toolError("record_get", err);
|
|
9245
9361
|
}
|
|
9246
9362
|
}
|
|
@@ -9300,7 +9416,11 @@ var record_create_default = recordCreate;
|
|
|
9300
9416
|
// src/tools/record_update.ts
|
|
9301
9417
|
var import_zod5 = require("zod");
|
|
9302
9418
|
var inputSchema5 = {
|
|
9303
|
-
id: import_zod5.z.string().
|
|
9419
|
+
id: import_zod5.z.string().optional().describe("The Vectros record id to update. Provide this OR externalId+type."),
|
|
9420
|
+
externalId: import_zod5.z.string().optional().describe(
|
|
9421
|
+
"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."
|
|
9422
|
+
),
|
|
9423
|
+
type: import_zod5.z.string().optional().describe("Record type \u2014 REQUIRED when selecting by `externalId` (externalId is unique within a type)."),
|
|
9304
9424
|
fields: import_zod5.z.record(import_zod5.z.string(), import_zod5.z.unknown()).describe(
|
|
9305
9425
|
"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."
|
|
9306
9426
|
),
|
|
@@ -9311,23 +9431,27 @@ var inputSchema5 = {
|
|
|
9311
9431
|
};
|
|
9312
9432
|
var recordUpdate = ({ client, log }) => ({
|
|
9313
9433
|
name: "record_update",
|
|
9314
|
-
title: "Update a record",
|
|
9315
|
-
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.",
|
|
9434
|
+
title: "Update a record by id or externalId",
|
|
9435
|
+
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.",
|
|
9316
9436
|
inputSchema: inputSchema5,
|
|
9317
9437
|
handler: async (args) => {
|
|
9318
9438
|
const id = args.id;
|
|
9439
|
+
const externalId = args.externalId;
|
|
9440
|
+
const type = args.type;
|
|
9319
9441
|
const fields = args.fields;
|
|
9320
9442
|
const status = args.status;
|
|
9321
9443
|
const expectedVersion = args.expectedVersion;
|
|
9322
9444
|
try {
|
|
9445
|
+
const resolved = await resolveRecordIdByExternalId(client, { id, externalId, type });
|
|
9446
|
+
if ("error" in resolved) return toolError("record_update", new Error(resolved.error));
|
|
9323
9447
|
const body = { payload: fields };
|
|
9324
9448
|
if (status !== void 0) body.status = status;
|
|
9325
9449
|
if (expectedVersion !== void 0) body.expectedVersion = expectedVersion;
|
|
9326
|
-
const updated = await client.records.patchRecord({ id, body });
|
|
9327
|
-
log.debug({ tool: "record_update", id }, "record_update ok");
|
|
9450
|
+
const updated = await client.records.patchRecord({ id: resolved.id, body });
|
|
9451
|
+
log.debug({ tool: "record_update", id: resolved.id, externalId }, "record_update ok");
|
|
9328
9452
|
return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
|
|
9329
9453
|
} catch (err) {
|
|
9330
|
-
log.warn({ tool: "record_update", id, err: String(err) }, "record_update failed");
|
|
9454
|
+
log.warn({ tool: "record_update", id, externalId, type, err: String(err) }, "record_update failed");
|
|
9331
9455
|
return toolError("record_update", err);
|
|
9332
9456
|
}
|
|
9333
9457
|
}
|
|
@@ -9594,7 +9718,11 @@ var list_schemas_default = listSchemas;
|
|
|
9594
9718
|
var import_zod10 = require("zod");
|
|
9595
9719
|
var MAX_TEXT_CHARS = 32e3;
|
|
9596
9720
|
var inputSchema10 = {
|
|
9597
|
-
documentId: import_zod10.z.string().
|
|
9721
|
+
documentId: import_zod10.z.string().optional().describe("The document ID returned by document_ingest or hybrid_search. Provide this OR externalId+type."),
|
|
9722
|
+
externalId: import_zod10.z.string().optional().describe(
|
|
9723
|
+
"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."
|
|
9724
|
+
),
|
|
9725
|
+
type: import_zod10.z.string().optional().describe("Document type \u2014 REQUIRED when selecting by `externalId` (externalId is unique within a type)."),
|
|
9598
9726
|
includeText: import_zod10.z.boolean().optional().describe(
|
|
9599
9727
|
"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."
|
|
9600
9728
|
),
|
|
@@ -9614,13 +9742,18 @@ function isNotAvailable(err) {
|
|
|
9614
9742
|
var documentGet = ({ client, log }) => ({
|
|
9615
9743
|
name: "document_get",
|
|
9616
9744
|
title: "Get document metadata (and optionally text)",
|
|
9617
|
-
description: "Fetch metadata for a single document by
|
|
9745
|
+
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`.",
|
|
9618
9746
|
inputSchema: inputSchema10,
|
|
9619
9747
|
handler: async (args) => {
|
|
9620
|
-
const
|
|
9748
|
+
const documentId = args.documentId;
|
|
9749
|
+
const externalId = args.externalId;
|
|
9750
|
+
const type = args.type;
|
|
9621
9751
|
const includeText = args.includeText ?? false;
|
|
9622
9752
|
const includeDownloadUrl = args.includeDownloadUrl ?? false;
|
|
9623
9753
|
try {
|
|
9754
|
+
const resolved = await resolveDocumentIdByExternalId(client, { id: documentId, externalId, type });
|
|
9755
|
+
if ("error" in resolved) return toolError("document_get", new Error(resolved.error));
|
|
9756
|
+
const id = resolved.id;
|
|
9624
9757
|
const doc = await client.documents.getDocument({ id });
|
|
9625
9758
|
let extra = {};
|
|
9626
9759
|
if (includeText) {
|
|
@@ -9633,7 +9766,7 @@ var documentGet = ({ client, log }) => ({
|
|
|
9633
9766
|
if (isNotAvailable(textErr)) {
|
|
9634
9767
|
log.debug(
|
|
9635
9768
|
{ tool: "document_get", id },
|
|
9636
|
-
"document_get text unavailable (
|
|
9769
|
+
"document_get text unavailable (not retained \u2014 storeText:false upload)"
|
|
9637
9770
|
);
|
|
9638
9771
|
extra = { ...extra, textAvailable: false };
|
|
9639
9772
|
} else {
|
|
@@ -9667,7 +9800,7 @@ var documentGet = ({ client, log }) => ({
|
|
|
9667
9800
|
]
|
|
9668
9801
|
};
|
|
9669
9802
|
} catch (err) {
|
|
9670
|
-
log.warn({ tool: "document_get",
|
|
9803
|
+
log.warn({ tool: "document_get", documentId, externalId, type, err: String(err) }, "document_get failed");
|
|
9671
9804
|
return toolError("document_get", err);
|
|
9672
9805
|
}
|
|
9673
9806
|
}
|
|
@@ -9782,13 +9915,22 @@ var document_query_default = documentQuery;
|
|
|
9782
9915
|
// src/tools/document_update.ts
|
|
9783
9916
|
var import_zod12 = require("zod");
|
|
9784
9917
|
var inputSchema12 = {
|
|
9785
|
-
documentId: import_zod12.z.string().
|
|
9918
|
+
documentId: import_zod12.z.string().optional().describe("The Vectros document id to update. Provide this OR externalId+type."),
|
|
9919
|
+
externalId: import_zod12.z.string().optional().describe(
|
|
9920
|
+
"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."
|
|
9921
|
+
),
|
|
9922
|
+
type: import_zod12.z.string().optional().describe("Document type \u2014 REQUIRED when selecting by `externalId` (externalId is unique within a type)."),
|
|
9786
9923
|
title: import_zod12.z.string().optional().describe("New title. Omit to keep the current title."),
|
|
9787
9924
|
fields: import_zod12.z.record(import_zod12.z.string(), import_zod12.z.unknown()).optional().describe(
|
|
9788
9925
|
"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."
|
|
9789
9926
|
),
|
|
9927
|
+
text: import_zod12.z.string().optional().describe(
|
|
9928
|
+
"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."
|
|
9929
|
+
),
|
|
9790
9930
|
folderId: import_zod12.z.string().optional().describe("Move the document into this folder (cannot be cleared once set)."),
|
|
9791
|
-
|
|
9931
|
+
status: import_zod12.z.enum(["ACTIVE", "ARCHIVED"]).optional().describe(
|
|
9932
|
+
"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."
|
|
9933
|
+
),
|
|
9792
9934
|
userId: import_zod12.z.string().optional().describe("Reassign the owning user (Vectros UUID)."),
|
|
9793
9935
|
orgId: import_zod12.z.string().optional().describe("Reassign the owning org (Vectros UUID)."),
|
|
9794
9936
|
clientId: import_zod12.z.string().optional().describe("Reassign the associated client (Vectros UUID)."),
|
|
@@ -9799,33 +9941,39 @@ var inputSchema12 = {
|
|
|
9799
9941
|
var documentUpdate = ({ client, log }) => ({
|
|
9800
9942
|
name: "document_update",
|
|
9801
9943
|
title: "Update a document",
|
|
9802
|
-
description:
|
|
9944
|
+
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.',
|
|
9803
9945
|
inputSchema: inputSchema12,
|
|
9804
9946
|
handler: async (args) => {
|
|
9805
9947
|
const documentId = args.documentId;
|
|
9948
|
+
const externalId = args.externalId;
|
|
9949
|
+
const type = args.type;
|
|
9806
9950
|
const title = args.title;
|
|
9807
9951
|
const fields = args.fields;
|
|
9952
|
+
const text = args.text;
|
|
9808
9953
|
const folderId = args.folderId;
|
|
9809
|
-
const
|
|
9954
|
+
const status = args.status;
|
|
9810
9955
|
const userId = args.userId;
|
|
9811
9956
|
const orgId = args.orgId;
|
|
9812
9957
|
const clientId = args.clientId;
|
|
9813
9958
|
const expectedVersion = args.expectedVersion;
|
|
9814
9959
|
try {
|
|
9960
|
+
const resolved = await resolveDocumentIdByExternalId(client, { id: documentId, externalId, type });
|
|
9961
|
+
if ("error" in resolved) return toolError("document_update", new Error(resolved.error));
|
|
9815
9962
|
const body = {};
|
|
9816
9963
|
if (title !== void 0) body.title = title;
|
|
9817
9964
|
if (fields !== void 0) body.payload = fields;
|
|
9965
|
+
if (text !== void 0) body.text = text;
|
|
9818
9966
|
if (folderId !== void 0) body.folderId = folderId;
|
|
9819
|
-
if (
|
|
9967
|
+
if (status !== void 0) body.status = status;
|
|
9820
9968
|
if (userId !== void 0) body.userId = userId;
|
|
9821
9969
|
if (orgId !== void 0) body.orgId = orgId;
|
|
9822
9970
|
if (clientId !== void 0) body.clientId = clientId;
|
|
9823
9971
|
if (expectedVersion !== void 0) body.expectedVersion = expectedVersion;
|
|
9824
9972
|
const updated = await client.documents.patchDocument({
|
|
9825
|
-
id:
|
|
9973
|
+
id: resolved.id,
|
|
9826
9974
|
body
|
|
9827
9975
|
});
|
|
9828
|
-
log.debug({ tool: "document_update", documentId }, "document_update ok");
|
|
9976
|
+
log.debug({ tool: "document_update", documentId: resolved.id, externalId }, "document_update ok");
|
|
9829
9977
|
return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
|
|
9830
9978
|
} catch (err) {
|
|
9831
9979
|
log.warn({ tool: "document_update", documentId, err: String(err) }, "document_update failed");
|
|
@@ -9843,7 +9991,7 @@ var inputSchema13 = {
|
|
|
9843
9991
|
var documentDelete = ({ client, log }) => ({
|
|
9844
9992
|
name: "document_delete",
|
|
9845
9993
|
title: "Delete a document",
|
|
9846
|
-
description:
|
|
9994
|
+
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).',
|
|
9847
9995
|
inputSchema: inputSchema13,
|
|
9848
9996
|
handler: async (args) => {
|
|
9849
9997
|
const documentId = args.documentId;
|
|
@@ -10123,6 +10271,9 @@ var inputSchema19 = {
|
|
|
10123
10271
|
externalId: import_zod18.z.string().optional().describe(
|
|
10124
10272
|
"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."
|
|
10125
10273
|
),
|
|
10274
|
+
upsert: import_zod18.z.boolean().optional().describe(
|
|
10275
|
+
"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)."
|
|
10276
|
+
),
|
|
10126
10277
|
schemaId: import_zod18.z.string().optional().describe(
|
|
10127
10278
|
"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."
|
|
10128
10279
|
),
|
|
@@ -10132,7 +10283,7 @@ var inputSchema19 = {
|
|
|
10132
10283
|
// Inline text mode (mutually exclusive with filePath):
|
|
10133
10284
|
text: import_zod18.z.string().min(1).optional().describe("Text body to ingest inline. Mutually exclusive with filePath. Either text or filePath is required."),
|
|
10134
10285
|
storeText: import_zod18.z.boolean().optional().describe(
|
|
10135
|
-
"
|
|
10286
|
+
"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."
|
|
10136
10287
|
),
|
|
10137
10288
|
// File upload mode (mutually exclusive with text; stdio-transport-only):
|
|
10138
10289
|
filePath: import_zod18.z.string().min(1).optional().describe(
|
|
@@ -10148,13 +10299,14 @@ var inputSchema19 = {
|
|
|
10148
10299
|
var documentIngest = ({ client, log, transport, ingestRoot }) => ({
|
|
10149
10300
|
name: "document_ingest",
|
|
10150
10301
|
title: "Ingest a new document (text body or file upload)",
|
|
10151
|
-
description: "Create a new document in
|
|
10302
|
+
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.",
|
|
10152
10303
|
inputSchema: inputSchema19,
|
|
10153
10304
|
handler: async (args) => {
|
|
10154
10305
|
const title = args.title;
|
|
10155
10306
|
const text = args.text;
|
|
10156
10307
|
const filePath = args.filePath;
|
|
10157
10308
|
const schemaId = args.schemaId;
|
|
10309
|
+
const upsert = args.upsert;
|
|
10158
10310
|
const indexMode = args.indexMode ?? (schemaId ? void 0 : "HYBRID");
|
|
10159
10311
|
if (!text && !filePath) {
|
|
10160
10312
|
return toolError(
|
|
@@ -10187,13 +10339,12 @@ var documentIngest = ({ client, log, transport, ingestRoot }) => ({
|
|
|
10187
10339
|
};
|
|
10188
10340
|
try {
|
|
10189
10341
|
if (text) {
|
|
10190
|
-
const storeText = args.storeText ?? true;
|
|
10191
10342
|
const result = await client.documents.ingestDocument({
|
|
10343
|
+
upsert,
|
|
10192
10344
|
body: {
|
|
10193
10345
|
title,
|
|
10194
10346
|
text,
|
|
10195
10347
|
indexMode,
|
|
10196
|
-
storeText,
|
|
10197
10348
|
...common
|
|
10198
10349
|
}
|
|
10199
10350
|
});
|
|
@@ -10224,10 +10375,13 @@ var documentIngest = ({ client, log, transport, ingestRoot }) => ({
|
|
|
10224
10375
|
}
|
|
10225
10376
|
const fileName = (0, import_node_path.basename)(safePath);
|
|
10226
10377
|
const fileType = args.fileType ?? inferMimeType(safePath);
|
|
10378
|
+
const storeTextFile = args.storeText ?? true;
|
|
10227
10379
|
const upload = await client.documents.uploadDocument({
|
|
10380
|
+
upsert,
|
|
10228
10381
|
fileName,
|
|
10229
10382
|
fileType,
|
|
10230
10383
|
indexMode,
|
|
10384
|
+
storeText: storeTextFile,
|
|
10231
10385
|
...common
|
|
10232
10386
|
});
|
|
10233
10387
|
const uploadUrl = upload?.uploadUrl;
|
|
@@ -10263,8 +10417,8 @@ var documentIngest = ({ client, log, transport, ingestRoot }) => ({
|
|
|
10263
10417
|
text: JSON.stringify(
|
|
10264
10418
|
{
|
|
10265
10419
|
...upload,
|
|
10266
|
-
|
|
10267
|
-
_note: "File uploaded; indexing is asynchronous. Poll document_get(id) until
|
|
10420
|
+
indexStatus: "PENDING_INDEX",
|
|
10421
|
+
_note: "File uploaded; indexing is asynchronous. Poll document_get(id) until indexStatus is INDEXED."
|
|
10268
10422
|
},
|
|
10269
10423
|
null,
|
|
10270
10424
|
2
|
|
@@ -10561,6 +10715,14 @@ function zodTypeToJsonSchema(schema) {
|
|
|
10561
10715
|
|
|
10562
10716
|
// src/server.ts
|
|
10563
10717
|
var DEFAULT_ENVIRONMENT = "https://api.vectros.ai";
|
|
10718
|
+
function formatArgError(tool, error) {
|
|
10719
|
+
const unknownKeys = error.issues.filter((i) => i.code === "unrecognized_keys").flatMap((i) => i.keys);
|
|
10720
|
+
if (unknownKeys.length > 0) {
|
|
10721
|
+
const valid = Object.keys(tool.inputSchema).join(", ");
|
|
10722
|
+
return `Unknown argument(s): ${unknownKeys.join(", ")}. Valid arguments for ${tool.name}: ${valid}. Pass only these \u2014 an unrecognized argument is rejected, not ignored.`;
|
|
10723
|
+
}
|
|
10724
|
+
return `Invalid arguments: ${error.message}`;
|
|
10725
|
+
}
|
|
10564
10726
|
var VectrosMCPServer = class {
|
|
10565
10727
|
server;
|
|
10566
10728
|
log;
|
|
@@ -10650,7 +10812,11 @@ var VectrosMCPServer = class {
|
|
|
10650
10812
|
name: t.name,
|
|
10651
10813
|
title: t.title,
|
|
10652
10814
|
description: t.description,
|
|
10653
|
-
|
|
10815
|
+
// additionalProperties:false advertises strict argument validation to the
|
|
10816
|
+
// MCP client (a well-behaved client rejects an unknown key before the call).
|
|
10817
|
+
// The server enforces it too (z.object(...).strict() below) so a raw JSON-RPC
|
|
10818
|
+
// call that skips client validation is still rejected — belt and suspenders.
|
|
10819
|
+
inputSchema: { ...zodShapeToJsonSchema(t.inputSchema), additionalProperties: false }
|
|
10654
10820
|
}))
|
|
10655
10821
|
})
|
|
10656
10822
|
);
|
|
@@ -10660,9 +10826,9 @@ var VectrosMCPServer = class {
|
|
|
10660
10826
|
if (!tool) {
|
|
10661
10827
|
result = toolError(req.params.name ?? "unknown", new Error("No such tool."));
|
|
10662
10828
|
} else {
|
|
10663
|
-
const parsed = import_zod22.z.object(tool.inputSchema).safeParse(req.params.arguments ?? {});
|
|
10829
|
+
const parsed = import_zod22.z.object(tool.inputSchema).strict().safeParse(req.params.arguments ?? {});
|
|
10664
10830
|
if (!parsed.success) {
|
|
10665
|
-
result = toolError(tool.name, new Error(
|
|
10831
|
+
result = toolError(tool.name, new Error(formatArgError(tool, parsed.error)));
|
|
10666
10832
|
} else {
|
|
10667
10833
|
try {
|
|
10668
10834
|
result = await tool.handler(
|