@vectros-ai/mcp-server 0.5.3 → 0.6.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 +54 -0
- package/README.md +17 -2
- package/dist/cli-http.js +206 -37
- package/dist/cli-http.js.map +1 -1
- package/dist/cli-http.mjs +206 -37
- package/dist/cli-http.mjs.map +1 -1
- package/dist/cli.js +206 -37
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +206 -37
- package/dist/cli.mjs.map +1 -1
- package/dist/index.js +206 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +206 -37
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/cli.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",
|
|
@@ -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.6.0" : "dev",
|
|
8768
|
+
sdk: true ? "0.32.0" : "dev"
|
|
8761
8769
|
};
|
|
8762
8770
|
function formatBuildInfo() {
|
|
8763
8771
|
return `vectros-mcp-server ${BUILD_INFO.mcpServer} (sdk ${BUILD_INFO.sdk})`;
|
|
@@ -8997,6 +9005,32 @@ ${detail}${pointer}`
|
|
|
8997
9005
|
// src/tools/hybrid_search.ts
|
|
8998
9006
|
var MCP_DEFAULT_LIMIT = 3;
|
|
8999
9007
|
var MCP_MAX_LIMIT = 10;
|
|
9008
|
+
var MCP_RESPONSE_CHAR_BUDGET = 24e3;
|
|
9009
|
+
var SNIPPET_MAX_CHARS = 160;
|
|
9010
|
+
var INTERNAL_METADATA_KEYS = /* @__PURE__ */ new Set(["tenantId", "owner_id", "model_type", "rootFolderId", "folderId"]);
|
|
9011
|
+
function trimMetadata(md) {
|
|
9012
|
+
if (!md || typeof md !== "object" || Array.isArray(md)) return md;
|
|
9013
|
+
const out = {};
|
|
9014
|
+
for (const [k, v] of Object.entries(md)) {
|
|
9015
|
+
if (!INTERNAL_METADATA_KEYS.has(k)) out[k] = v;
|
|
9016
|
+
}
|
|
9017
|
+
return out;
|
|
9018
|
+
}
|
|
9019
|
+
function reshapeHit(hit, includeContext) {
|
|
9020
|
+
const out = { ...hit };
|
|
9021
|
+
if ("metadata" in out) out.metadata = trimMetadata(out.metadata);
|
|
9022
|
+
const chunk = typeof hit.chunkText === "string" ? hit.chunkText : void 0;
|
|
9023
|
+
const ctx = typeof hit.contextText === "string" ? hit.contextText : void 0;
|
|
9024
|
+
if (includeContext) {
|
|
9025
|
+
if (chunk && ctx && ctx.includes(chunk)) delete out.chunkText;
|
|
9026
|
+
} else {
|
|
9027
|
+
delete out.contextText;
|
|
9028
|
+
}
|
|
9029
|
+
if ((out.snippet == null || out.snippet === "") && chunk) {
|
|
9030
|
+
out.snippet = chunk.length > SNIPPET_MAX_CHARS ? chunk.slice(0, SNIPPET_MAX_CHARS) + "\u2026" : chunk;
|
|
9031
|
+
}
|
|
9032
|
+
return out;
|
|
9033
|
+
}
|
|
9000
9034
|
var inputSchema = {
|
|
9001
9035
|
query: z.string().min(1, "query must be non-empty").describe("Natural-language or keyword query."),
|
|
9002
9036
|
mode: z.enum(["HYBRID", "TEXT", "SEMANTIC"]).optional().describe("HYBRID (BM25 + dense, default), TEXT (BM25 only), or SEMANTIC (dense only)."),
|
|
@@ -9011,7 +9045,9 @@ var inputSchema = {
|
|
|
9011
9045
|
rootFolderId: z.string().optional().describe("Scope to content under this folder AND all its descendants (subtree-root folder id)."),
|
|
9012
9046
|
// Content narrowing.
|
|
9013
9047
|
contentTypes: z.array(z.enum(["documents", "records"])).optional().describe('Narrow to content types. ["documents"] or ["records"]; omit for unified (both).'),
|
|
9014
|
-
typeName: z.string().optional().describe(
|
|
9048
|
+
typeName: z.string().optional().describe(
|
|
9049
|
+
'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.'
|
|
9050
|
+
),
|
|
9015
9051
|
filters: z.record(z.unknown()).optional().describe(
|
|
9016
9052
|
'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}}.'
|
|
9017
9053
|
),
|
|
@@ -9026,6 +9062,9 @@ var inputSchema = {
|
|
|
9026
9062
|
minSimilarity: z.number().min(0).max(1).optional().describe("Minimum semantic similarity (0.0\u20131.0); results below are excluded."),
|
|
9027
9063
|
minTextRelevance: z.number().min(0).max(1).optional().describe("Minimum relative keyword (BM25) relevance (0.0\u20131.0); the keyword-leg analogue of minSimilarity."),
|
|
9028
9064
|
uniqueDocuments: z.boolean().optional().describe("When true, return at most one chunk per source document."),
|
|
9065
|
+
includeContext: z.boolean().optional().describe(
|
|
9066
|
+
"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."
|
|
9067
|
+
),
|
|
9029
9068
|
requireComplete: z.boolean().optional().describe(
|
|
9030
9069
|
"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)."
|
|
9031
9070
|
)
|
|
@@ -9033,7 +9072,7 @@ var inputSchema = {
|
|
|
9033
9072
|
var hybridSearch = ({ client, log }) => ({
|
|
9034
9073
|
name: "hybrid_search",
|
|
9035
9074
|
title: "Hybrid search",
|
|
9036
|
-
description:
|
|
9075
|
+
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".`,
|
|
9037
9076
|
inputSchema,
|
|
9038
9077
|
handler: async (args) => {
|
|
9039
9078
|
const limit = args.limit ?? MCP_DEFAULT_LIMIT;
|
|
@@ -9060,13 +9099,30 @@ var hybridSearch = ({ client, log }) => ({
|
|
|
9060
9099
|
uniqueDocuments: args.uniqueDocuments,
|
|
9061
9100
|
requireComplete: args.requireComplete
|
|
9062
9101
|
});
|
|
9102
|
+
const hits = Array.isArray(
|
|
9103
|
+
result.results
|
|
9104
|
+
) ? result.results : [];
|
|
9105
|
+
const includeContext = args.includeContext ?? false;
|
|
9106
|
+
let shaped = hits.map((h) => reshapeHit(h, includeContext));
|
|
9107
|
+
const textLegEmpty = mode !== "SEMANTIC" && hits.length > 0 && hits.every((h) => !(Number(h.textScore) > 0));
|
|
9108
|
+
const envelope = {
|
|
9109
|
+
...result,
|
|
9110
|
+
results: shaped
|
|
9111
|
+
};
|
|
9112
|
+
if (textLegEmpty) envelope.textLegEmpty = true;
|
|
9113
|
+
let text = JSON.stringify(envelope, null, 2);
|
|
9114
|
+
while (shaped.length > 1 && text.length > MCP_RESPONSE_CHAR_BUDGET) {
|
|
9115
|
+
shaped = shaped.slice(0, -1);
|
|
9116
|
+
envelope.results = shaped;
|
|
9117
|
+
envelope.truncated = true;
|
|
9118
|
+
envelope.returnedResults = shaped.length;
|
|
9119
|
+
text = JSON.stringify(envelope, null, 2);
|
|
9120
|
+
}
|
|
9063
9121
|
log.debug(
|
|
9064
|
-
{ tool: "hybrid_search", mode, limit, returned:
|
|
9122
|
+
{ tool: "hybrid_search", mode, limit, returned: shaped.length, textLegEmpty },
|
|
9065
9123
|
"hybrid_search ok"
|
|
9066
9124
|
);
|
|
9067
|
-
return {
|
|
9068
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
9069
|
-
};
|
|
9125
|
+
return { content: [{ type: "text", text }] };
|
|
9070
9126
|
} catch (err) {
|
|
9071
9127
|
log.warn({ tool: "hybrid_search", err: String(err) }, "hybrid_search failed");
|
|
9072
9128
|
return toolError("hybrid_search", err);
|
|
@@ -9101,7 +9157,9 @@ var MCP_MAX_LIMIT2 = 10;
|
|
|
9101
9157
|
var inputSchema2 = {
|
|
9102
9158
|
type: z2.string().min(1, "type (record type) is required").describe('Record type / schema name (e.g. "patient", "clinical_note").'),
|
|
9103
9159
|
// Lookup-mode args — provide `field` plus EXACTLY ONE of: value | from+to | prefix.
|
|
9104
|
-
field: z2.string().optional().describe(
|
|
9160
|
+
field: z2.string().optional().describe(
|
|
9161
|
+
'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.)'
|
|
9162
|
+
),
|
|
9105
9163
|
value: z2.string().optional().describe("Lookup mode (equality): exact-match value for `field`."),
|
|
9106
9164
|
from: z2.string().optional().describe("Lookup mode (range): inclusive lower bound; requires `to`. Non-sensitive fields only."),
|
|
9107
9165
|
to: z2.string().optional().describe("Lookup mode (range): inclusive upper bound; requires `from`."),
|
|
@@ -9196,19 +9254,77 @@ var record_query_default = recordQuery;
|
|
|
9196
9254
|
|
|
9197
9255
|
// src/tools/record_get.ts
|
|
9198
9256
|
import { z as z3 } from "zod";
|
|
9257
|
+
|
|
9258
|
+
// src/tools/resolve-external-id.ts
|
|
9259
|
+
function validateSelector(sel, entity) {
|
|
9260
|
+
const hasId = sel.id !== void 0 && sel.id !== "";
|
|
9261
|
+
const hasExt = sel.externalId !== void 0 && sel.externalId !== "";
|
|
9262
|
+
if (hasId && hasExt) {
|
|
9263
|
+
return `Provide either id or externalId (not both) to identify the ${entity}.`;
|
|
9264
|
+
}
|
|
9265
|
+
if (!hasId && !hasExt) {
|
|
9266
|
+
return `Provide id, or externalId + type, to identify the ${entity}.`;
|
|
9267
|
+
}
|
|
9268
|
+
if (hasExt && (sel.type === void 0 || sel.type === "")) {
|
|
9269
|
+
return "type is required when selecting by externalId (externalId is unique within a type).";
|
|
9270
|
+
}
|
|
9271
|
+
return void 0;
|
|
9272
|
+
}
|
|
9273
|
+
async function resolveRecordIdByExternalId(client, sel) {
|
|
9274
|
+
const err = validateSelector(sel, "record");
|
|
9275
|
+
if (err) return { error: err };
|
|
9276
|
+
if (sel.id) return { id: sel.id };
|
|
9277
|
+
const page = await client.records.lookupRecordsByBody({
|
|
9278
|
+
type: sel.type,
|
|
9279
|
+
field: "externalId",
|
|
9280
|
+
value: sel.externalId,
|
|
9281
|
+
limit: 1
|
|
9282
|
+
});
|
|
9283
|
+
const items = pageItems(page);
|
|
9284
|
+
if (items.length === 0) {
|
|
9285
|
+
return { error: `No record of type '${sel.type}' with externalId '${sel.externalId}'.` };
|
|
9286
|
+
}
|
|
9287
|
+
return { id: items[0].id };
|
|
9288
|
+
}
|
|
9289
|
+
async function resolveDocumentIdByExternalId(client, sel) {
|
|
9290
|
+
const err = validateSelector(sel, "document");
|
|
9291
|
+
if (err) return { error: err };
|
|
9292
|
+
if (sel.id) return { id: sel.id };
|
|
9293
|
+
const page = await client.documents.lookupDocumentsByBody({
|
|
9294
|
+
type: sel.type,
|
|
9295
|
+
field: "externalId",
|
|
9296
|
+
value: sel.externalId,
|
|
9297
|
+
limit: 1
|
|
9298
|
+
});
|
|
9299
|
+
const items = pageItems(page);
|
|
9300
|
+
if (items.length === 0) {
|
|
9301
|
+
return { error: `No document of type '${sel.type}' with externalId '${sel.externalId}'.` };
|
|
9302
|
+
}
|
|
9303
|
+
return { id: items[0].id };
|
|
9304
|
+
}
|
|
9305
|
+
|
|
9306
|
+
// src/tools/record_get.ts
|
|
9199
9307
|
var MAX_PAYLOAD_CHARS = 32e3;
|
|
9200
9308
|
var inputSchema3 = {
|
|
9201
|
-
id: z3.string().
|
|
9309
|
+
id: z3.string().optional().describe("The Vectros record id (e.g. from record_query or hybrid_search). Provide this OR externalId+type."),
|
|
9310
|
+
externalId: z3.string().optional().describe(
|
|
9311
|
+
"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."
|
|
9312
|
+
),
|
|
9313
|
+
type: z3.string().optional().describe("Record type \u2014 REQUIRED when selecting by `externalId` (externalId is unique within a type).")
|
|
9202
9314
|
};
|
|
9203
9315
|
var recordGet = ({ client, log }) => ({
|
|
9204
9316
|
name: "record_get",
|
|
9205
|
-
title: "Get a record by id",
|
|
9206
|
-
description: "Fetch a single structured record
|
|
9317
|
+
title: "Get a record by id or externalId",
|
|
9318
|
+
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.",
|
|
9207
9319
|
inputSchema: inputSchema3,
|
|
9208
9320
|
handler: async (args) => {
|
|
9209
9321
|
const id = args.id;
|
|
9322
|
+
const externalId = args.externalId;
|
|
9323
|
+
const type = args.type;
|
|
9210
9324
|
try {
|
|
9211
|
-
const
|
|
9325
|
+
const resolved = await resolveRecordIdByExternalId(client, { id, externalId, type });
|
|
9326
|
+
if ("error" in resolved) return toolError("record_get", new Error(resolved.error));
|
|
9327
|
+
const record = await client.records.getRecord({ id: resolved.id });
|
|
9212
9328
|
const out = { ...record };
|
|
9213
9329
|
if (record.payload !== void 0) {
|
|
9214
9330
|
const json = JSON.stringify(record.payload);
|
|
@@ -9220,12 +9336,12 @@ var recordGet = ({ client, log }) => ({
|
|
|
9220
9336
|
}
|
|
9221
9337
|
}
|
|
9222
9338
|
log.debug(
|
|
9223
|
-
{ tool: "record_get", id, truncated: out.payloadTruncated === true },
|
|
9339
|
+
{ tool: "record_get", id: resolved.id, externalId, truncated: out.payloadTruncated === true },
|
|
9224
9340
|
"record_get ok"
|
|
9225
9341
|
);
|
|
9226
9342
|
return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
|
|
9227
9343
|
} catch (err) {
|
|
9228
|
-
log.warn({ tool: "record_get", id, err: String(err) }, "record_get failed");
|
|
9344
|
+
log.warn({ tool: "record_get", id, externalId, type, err: String(err) }, "record_get failed");
|
|
9229
9345
|
return toolError("record_get", err);
|
|
9230
9346
|
}
|
|
9231
9347
|
}
|
|
@@ -9285,7 +9401,11 @@ var record_create_default = recordCreate;
|
|
|
9285
9401
|
// src/tools/record_update.ts
|
|
9286
9402
|
import { z as z5 } from "zod";
|
|
9287
9403
|
var inputSchema5 = {
|
|
9288
|
-
id: z5.string().
|
|
9404
|
+
id: z5.string().optional().describe("The Vectros record id to update. Provide this OR externalId+type."),
|
|
9405
|
+
externalId: z5.string().optional().describe(
|
|
9406
|
+
"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."
|
|
9407
|
+
),
|
|
9408
|
+
type: z5.string().optional().describe("Record type \u2014 REQUIRED when selecting by `externalId` (externalId is unique within a type)."),
|
|
9289
9409
|
fields: z5.record(z5.string(), z5.unknown()).describe(
|
|
9290
9410
|
"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."
|
|
9291
9411
|
),
|
|
@@ -9296,23 +9416,27 @@ var inputSchema5 = {
|
|
|
9296
9416
|
};
|
|
9297
9417
|
var recordUpdate = ({ client, log }) => ({
|
|
9298
9418
|
name: "record_update",
|
|
9299
|
-
title: "Update a record",
|
|
9300
|
-
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.",
|
|
9419
|
+
title: "Update a record by id or externalId",
|
|
9420
|
+
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.",
|
|
9301
9421
|
inputSchema: inputSchema5,
|
|
9302
9422
|
handler: async (args) => {
|
|
9303
9423
|
const id = args.id;
|
|
9424
|
+
const externalId = args.externalId;
|
|
9425
|
+
const type = args.type;
|
|
9304
9426
|
const fields = args.fields;
|
|
9305
9427
|
const status = args.status;
|
|
9306
9428
|
const expectedVersion = args.expectedVersion;
|
|
9307
9429
|
try {
|
|
9430
|
+
const resolved = await resolveRecordIdByExternalId(client, { id, externalId, type });
|
|
9431
|
+
if ("error" in resolved) return toolError("record_update", new Error(resolved.error));
|
|
9308
9432
|
const body = { payload: fields };
|
|
9309
9433
|
if (status !== void 0) body.status = status;
|
|
9310
9434
|
if (expectedVersion !== void 0) body.expectedVersion = expectedVersion;
|
|
9311
|
-
const updated = await client.records.patchRecord({ id, body });
|
|
9312
|
-
log.debug({ tool: "record_update", id }, "record_update ok");
|
|
9435
|
+
const updated = await client.records.patchRecord({ id: resolved.id, body });
|
|
9436
|
+
log.debug({ tool: "record_update", id: resolved.id, externalId }, "record_update ok");
|
|
9313
9437
|
return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
|
|
9314
9438
|
} catch (err) {
|
|
9315
|
-
log.warn({ tool: "record_update", id, err: String(err) }, "record_update failed");
|
|
9439
|
+
log.warn({ tool: "record_update", id, externalId, type, err: String(err) }, "record_update failed");
|
|
9316
9440
|
return toolError("record_update", err);
|
|
9317
9441
|
}
|
|
9318
9442
|
}
|
|
@@ -9579,7 +9703,11 @@ var list_schemas_default = listSchemas;
|
|
|
9579
9703
|
import { z as z10 } from "zod";
|
|
9580
9704
|
var MAX_TEXT_CHARS = 32e3;
|
|
9581
9705
|
var inputSchema10 = {
|
|
9582
|
-
documentId: z10.string().
|
|
9706
|
+
documentId: z10.string().optional().describe("The document ID returned by document_ingest or hybrid_search. Provide this OR externalId+type."),
|
|
9707
|
+
externalId: z10.string().optional().describe(
|
|
9708
|
+
"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."
|
|
9709
|
+
),
|
|
9710
|
+
type: z10.string().optional().describe("Document type \u2014 REQUIRED when selecting by `externalId` (externalId is unique within a type)."),
|
|
9583
9711
|
includeText: z10.boolean().optional().describe(
|
|
9584
9712
|
"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."
|
|
9585
9713
|
),
|
|
@@ -9599,13 +9727,18 @@ function isNotAvailable(err) {
|
|
|
9599
9727
|
var documentGet = ({ client, log }) => ({
|
|
9600
9728
|
name: "document_get",
|
|
9601
9729
|
title: "Get document metadata (and optionally text)",
|
|
9602
|
-
description: "Fetch metadata for a single document by
|
|
9730
|
+
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). If the document was ingested without `storeText: true`, text is unavailable \u2014 `textAvailable: false` flag is set and the agent should use `document_ask` for question-driven extraction instead. 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`.",
|
|
9603
9731
|
inputSchema: inputSchema10,
|
|
9604
9732
|
handler: async (args) => {
|
|
9605
|
-
const
|
|
9733
|
+
const documentId = args.documentId;
|
|
9734
|
+
const externalId = args.externalId;
|
|
9735
|
+
const type = args.type;
|
|
9606
9736
|
const includeText = args.includeText ?? false;
|
|
9607
9737
|
const includeDownloadUrl = args.includeDownloadUrl ?? false;
|
|
9608
9738
|
try {
|
|
9739
|
+
const resolved = await resolveDocumentIdByExternalId(client, { id: documentId, externalId, type });
|
|
9740
|
+
if ("error" in resolved) return toolError("document_get", new Error(resolved.error));
|
|
9741
|
+
const id = resolved.id;
|
|
9609
9742
|
const doc = await client.documents.getDocument({ id });
|
|
9610
9743
|
let extra = {};
|
|
9611
9744
|
if (includeText) {
|
|
@@ -9652,7 +9785,7 @@ var documentGet = ({ client, log }) => ({
|
|
|
9652
9785
|
]
|
|
9653
9786
|
};
|
|
9654
9787
|
} catch (err) {
|
|
9655
|
-
log.warn({ tool: "document_get",
|
|
9788
|
+
log.warn({ tool: "document_get", documentId, externalId, type, err: String(err) }, "document_get failed");
|
|
9656
9789
|
return toolError("document_get", err);
|
|
9657
9790
|
}
|
|
9658
9791
|
}
|
|
@@ -9767,13 +9900,23 @@ var document_query_default = documentQuery;
|
|
|
9767
9900
|
// src/tools/document_update.ts
|
|
9768
9901
|
import { z as z12 } from "zod";
|
|
9769
9902
|
var inputSchema12 = {
|
|
9770
|
-
documentId: z12.string().
|
|
9903
|
+
documentId: z12.string().optional().describe("The Vectros document id to update. Provide this OR externalId+type."),
|
|
9904
|
+
externalId: z12.string().optional().describe(
|
|
9905
|
+
"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."
|
|
9906
|
+
),
|
|
9907
|
+
type: z12.string().optional().describe("Document type \u2014 REQUIRED when selecting by `externalId` (externalId is unique within a type)."),
|
|
9771
9908
|
title: z12.string().optional().describe("New title. Omit to keep the current title."),
|
|
9772
9909
|
fields: z12.record(z12.string(), z12.unknown()).optional().describe(
|
|
9773
9910
|
"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."
|
|
9774
9911
|
),
|
|
9912
|
+
text: z12.string().optional().describe(
|
|
9913
|
+
"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."
|
|
9914
|
+
),
|
|
9775
9915
|
folderId: z12.string().optional().describe("Move the document into this folder (cannot be cleared once set)."),
|
|
9776
9916
|
storeText: z12.boolean().optional().describe("Whether the raw text is stored + retrievable."),
|
|
9917
|
+
status: z12.enum(["ACTIVE", "ARCHIVED"]).optional().describe(
|
|
9918
|
+
"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."
|
|
9919
|
+
),
|
|
9777
9920
|
userId: z12.string().optional().describe("Reassign the owning user (Vectros UUID)."),
|
|
9778
9921
|
orgId: z12.string().optional().describe("Reassign the owning org (Vectros UUID)."),
|
|
9779
9922
|
clientId: z12.string().optional().describe("Reassign the associated client (Vectros UUID)."),
|
|
@@ -9784,33 +9927,41 @@ var inputSchema12 = {
|
|
|
9784
9927
|
var documentUpdate = ({ client, log }) => ({
|
|
9785
9928
|
name: "document_update",
|
|
9786
9929
|
title: "Update a document",
|
|
9787
|
-
description:
|
|
9930
|
+
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.',
|
|
9788
9931
|
inputSchema: inputSchema12,
|
|
9789
9932
|
handler: async (args) => {
|
|
9790
9933
|
const documentId = args.documentId;
|
|
9934
|
+
const externalId = args.externalId;
|
|
9935
|
+
const type = args.type;
|
|
9791
9936
|
const title = args.title;
|
|
9792
9937
|
const fields = args.fields;
|
|
9938
|
+
const text = args.text;
|
|
9793
9939
|
const folderId = args.folderId;
|
|
9794
9940
|
const storeText = args.storeText;
|
|
9941
|
+
const status = args.status;
|
|
9795
9942
|
const userId = args.userId;
|
|
9796
9943
|
const orgId = args.orgId;
|
|
9797
9944
|
const clientId = args.clientId;
|
|
9798
9945
|
const expectedVersion = args.expectedVersion;
|
|
9799
9946
|
try {
|
|
9947
|
+
const resolved = await resolveDocumentIdByExternalId(client, { id: documentId, externalId, type });
|
|
9948
|
+
if ("error" in resolved) return toolError("document_update", new Error(resolved.error));
|
|
9800
9949
|
const body = {};
|
|
9801
9950
|
if (title !== void 0) body.title = title;
|
|
9802
9951
|
if (fields !== void 0) body.payload = fields;
|
|
9952
|
+
if (text !== void 0) body.text = text;
|
|
9803
9953
|
if (folderId !== void 0) body.folderId = folderId;
|
|
9804
9954
|
if (storeText !== void 0) body.storeText = storeText;
|
|
9955
|
+
if (status !== void 0) body.status = status;
|
|
9805
9956
|
if (userId !== void 0) body.userId = userId;
|
|
9806
9957
|
if (orgId !== void 0) body.orgId = orgId;
|
|
9807
9958
|
if (clientId !== void 0) body.clientId = clientId;
|
|
9808
9959
|
if (expectedVersion !== void 0) body.expectedVersion = expectedVersion;
|
|
9809
9960
|
const updated = await client.documents.patchDocument({
|
|
9810
|
-
id:
|
|
9961
|
+
id: resolved.id,
|
|
9811
9962
|
body
|
|
9812
9963
|
});
|
|
9813
|
-
log.debug({ tool: "document_update", documentId }, "document_update ok");
|
|
9964
|
+
log.debug({ tool: "document_update", documentId: resolved.id, externalId }, "document_update ok");
|
|
9814
9965
|
return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
|
|
9815
9966
|
} catch (err) {
|
|
9816
9967
|
log.warn({ tool: "document_update", documentId, err: String(err) }, "document_update failed");
|
|
@@ -9828,7 +9979,7 @@ var inputSchema13 = {
|
|
|
9828
9979
|
var documentDelete = ({ client, log }) => ({
|
|
9829
9980
|
name: "document_delete",
|
|
9830
9981
|
title: "Delete a document",
|
|
9831
|
-
description:
|
|
9982
|
+
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).',
|
|
9832
9983
|
inputSchema: inputSchema13,
|
|
9833
9984
|
handler: async (args) => {
|
|
9834
9985
|
const documentId = args.documentId;
|
|
@@ -10108,6 +10259,9 @@ var inputSchema19 = {
|
|
|
10108
10259
|
externalId: z18.string().optional().describe(
|
|
10109
10260
|
"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."
|
|
10110
10261
|
),
|
|
10262
|
+
upsert: z18.boolean().optional().describe(
|
|
10263
|
+
"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)."
|
|
10264
|
+
),
|
|
10111
10265
|
schemaId: z18.string().optional().describe(
|
|
10112
10266
|
"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."
|
|
10113
10267
|
),
|
|
@@ -10133,13 +10287,14 @@ var inputSchema19 = {
|
|
|
10133
10287
|
var documentIngest = ({ client, log, transport, ingestRoot }) => ({
|
|
10134
10288
|
name: "document_ingest",
|
|
10135
10289
|
title: "Ingest a new document (text body or file upload)",
|
|
10136
|
-
description: "Create a new document in
|
|
10290
|
+
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 (set false to skip storing the text body).",
|
|
10137
10291
|
inputSchema: inputSchema19,
|
|
10138
10292
|
handler: async (args) => {
|
|
10139
10293
|
const title = args.title;
|
|
10140
10294
|
const text = args.text;
|
|
10141
10295
|
const filePath = args.filePath;
|
|
10142
10296
|
const schemaId = args.schemaId;
|
|
10297
|
+
const upsert = args.upsert;
|
|
10143
10298
|
const indexMode = args.indexMode ?? (schemaId ? void 0 : "HYBRID");
|
|
10144
10299
|
if (!text && !filePath) {
|
|
10145
10300
|
return toolError(
|
|
@@ -10174,6 +10329,7 @@ var documentIngest = ({ client, log, transport, ingestRoot }) => ({
|
|
|
10174
10329
|
if (text) {
|
|
10175
10330
|
const storeText = args.storeText ?? true;
|
|
10176
10331
|
const result = await client.documents.ingestDocument({
|
|
10332
|
+
upsert,
|
|
10177
10333
|
body: {
|
|
10178
10334
|
title,
|
|
10179
10335
|
text,
|
|
@@ -10210,6 +10366,7 @@ var documentIngest = ({ client, log, transport, ingestRoot }) => ({
|
|
|
10210
10366
|
const fileName = basename(safePath);
|
|
10211
10367
|
const fileType = args.fileType ?? inferMimeType(safePath);
|
|
10212
10368
|
const upload = await client.documents.uploadDocument({
|
|
10369
|
+
upsert,
|
|
10213
10370
|
fileName,
|
|
10214
10371
|
fileType,
|
|
10215
10372
|
indexMode,
|
|
@@ -10248,8 +10405,8 @@ var documentIngest = ({ client, log, transport, ingestRoot }) => ({
|
|
|
10248
10405
|
text: JSON.stringify(
|
|
10249
10406
|
{
|
|
10250
10407
|
...upload,
|
|
10251
|
-
|
|
10252
|
-
_note: "File uploaded; indexing is asynchronous. Poll document_get(id) until
|
|
10408
|
+
indexStatus: "PENDING_INDEX",
|
|
10409
|
+
_note: "File uploaded; indexing is asynchronous. Poll document_get(id) until indexStatus is INDEXED."
|
|
10253
10410
|
},
|
|
10254
10411
|
null,
|
|
10255
10412
|
2
|
|
@@ -10546,6 +10703,14 @@ function zodTypeToJsonSchema(schema) {
|
|
|
10546
10703
|
|
|
10547
10704
|
// src/server.ts
|
|
10548
10705
|
var DEFAULT_ENVIRONMENT = "https://api.vectros.ai";
|
|
10706
|
+
function formatArgError(tool, error) {
|
|
10707
|
+
const unknownKeys = error.issues.filter((i) => i.code === "unrecognized_keys").flatMap((i) => i.keys);
|
|
10708
|
+
if (unknownKeys.length > 0) {
|
|
10709
|
+
const valid = Object.keys(tool.inputSchema).join(", ");
|
|
10710
|
+
return `Unknown argument(s): ${unknownKeys.join(", ")}. Valid arguments for ${tool.name}: ${valid}. Pass only these \u2014 an unrecognized argument is rejected, not ignored.`;
|
|
10711
|
+
}
|
|
10712
|
+
return `Invalid arguments: ${error.message}`;
|
|
10713
|
+
}
|
|
10549
10714
|
var VectrosMCPServer = class {
|
|
10550
10715
|
server;
|
|
10551
10716
|
log;
|
|
@@ -10635,7 +10800,11 @@ var VectrosMCPServer = class {
|
|
|
10635
10800
|
name: t.name,
|
|
10636
10801
|
title: t.title,
|
|
10637
10802
|
description: t.description,
|
|
10638
|
-
|
|
10803
|
+
// additionalProperties:false advertises strict argument validation to the
|
|
10804
|
+
// MCP client (a well-behaved client rejects an unknown key before the call).
|
|
10805
|
+
// The server enforces it too (z.object(...).strict() below) so a raw JSON-RPC
|
|
10806
|
+
// call that skips client validation is still rejected — belt and suspenders.
|
|
10807
|
+
inputSchema: { ...zodShapeToJsonSchema(t.inputSchema), additionalProperties: false }
|
|
10639
10808
|
}))
|
|
10640
10809
|
})
|
|
10641
10810
|
);
|
|
@@ -10645,9 +10814,9 @@ var VectrosMCPServer = class {
|
|
|
10645
10814
|
if (!tool) {
|
|
10646
10815
|
result = toolError(req.params.name ?? "unknown", new Error("No such tool."));
|
|
10647
10816
|
} else {
|
|
10648
|
-
const parsed = z22.object(tool.inputSchema).safeParse(req.params.arguments ?? {});
|
|
10817
|
+
const parsed = z22.object(tool.inputSchema).strict().safeParse(req.params.arguments ?? {});
|
|
10649
10818
|
if (!parsed.success) {
|
|
10650
|
-
result = toolError(tool.name, new Error(
|
|
10819
|
+
result = toolError(tool.name, new Error(formatArgError(tool, parsed.error)));
|
|
10651
10820
|
} else {
|
|
10652
10821
|
try {
|
|
10653
10822
|
result = await tool.handler(
|