@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.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",
|
|
@@ -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.6.0" : "dev",
|
|
8786
|
+
sdk: true ? "0.32.0" : "dev"
|
|
8779
8787
|
};
|
|
8780
8788
|
function formatBuildInfo() {
|
|
8781
8789
|
return `vectros-mcp-server ${BUILD_INFO.mcpServer} (sdk ${BUILD_INFO.sdk})`;
|
|
@@ -9015,6 +9023,32 @@ ${detail}${pointer}`
|
|
|
9015
9023
|
// src/tools/hybrid_search.ts
|
|
9016
9024
|
var MCP_DEFAULT_LIMIT = 3;
|
|
9017
9025
|
var MCP_MAX_LIMIT = 10;
|
|
9026
|
+
var MCP_RESPONSE_CHAR_BUDGET = 24e3;
|
|
9027
|
+
var SNIPPET_MAX_CHARS = 160;
|
|
9028
|
+
var INTERNAL_METADATA_KEYS = /* @__PURE__ */ new Set(["tenantId", "owner_id", "model_type", "rootFolderId", "folderId"]);
|
|
9029
|
+
function trimMetadata(md) {
|
|
9030
|
+
if (!md || typeof md !== "object" || Array.isArray(md)) return md;
|
|
9031
|
+
const out = {};
|
|
9032
|
+
for (const [k, v] of Object.entries(md)) {
|
|
9033
|
+
if (!INTERNAL_METADATA_KEYS.has(k)) out[k] = v;
|
|
9034
|
+
}
|
|
9035
|
+
return out;
|
|
9036
|
+
}
|
|
9037
|
+
function reshapeHit(hit, includeContext) {
|
|
9038
|
+
const out = { ...hit };
|
|
9039
|
+
if ("metadata" in out) out.metadata = trimMetadata(out.metadata);
|
|
9040
|
+
const chunk = typeof hit.chunkText === "string" ? hit.chunkText : void 0;
|
|
9041
|
+
const ctx = typeof hit.contextText === "string" ? hit.contextText : void 0;
|
|
9042
|
+
if (includeContext) {
|
|
9043
|
+
if (chunk && ctx && ctx.includes(chunk)) delete out.chunkText;
|
|
9044
|
+
} else {
|
|
9045
|
+
delete out.contextText;
|
|
9046
|
+
}
|
|
9047
|
+
if ((out.snippet == null || out.snippet === "") && chunk) {
|
|
9048
|
+
out.snippet = chunk.length > SNIPPET_MAX_CHARS ? chunk.slice(0, SNIPPET_MAX_CHARS) + "\u2026" : chunk;
|
|
9049
|
+
}
|
|
9050
|
+
return out;
|
|
9051
|
+
}
|
|
9018
9052
|
var inputSchema = {
|
|
9019
9053
|
query: import_zod.z.string().min(1, "query must be non-empty").describe("Natural-language or keyword query."),
|
|
9020
9054
|
mode: import_zod.z.enum(["HYBRID", "TEXT", "SEMANTIC"]).optional().describe("HYBRID (BM25 + dense, default), TEXT (BM25 only), or SEMANTIC (dense only)."),
|
|
@@ -9029,7 +9063,9 @@ var inputSchema = {
|
|
|
9029
9063
|
rootFolderId: import_zod.z.string().optional().describe("Scope to content under this folder AND all its descendants (subtree-root folder id)."),
|
|
9030
9064
|
// Content narrowing.
|
|
9031
9065
|
contentTypes: import_zod.z.array(import_zod.z.enum(["documents", "records"])).optional().describe('Narrow to content types. ["documents"] or ["records"]; omit for unified (both).'),
|
|
9032
|
-
typeName: import_zod.z.string().optional().describe(
|
|
9066
|
+
typeName: import_zod.z.string().optional().describe(
|
|
9067
|
+
'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.'
|
|
9068
|
+
),
|
|
9033
9069
|
filters: import_zod.z.record(import_zod.z.unknown()).optional().describe(
|
|
9034
9070
|
'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}}.'
|
|
9035
9071
|
),
|
|
@@ -9044,6 +9080,9 @@ var inputSchema = {
|
|
|
9044
9080
|
minSimilarity: import_zod.z.number().min(0).max(1).optional().describe("Minimum semantic similarity (0.0\u20131.0); results below are excluded."),
|
|
9045
9081
|
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."),
|
|
9046
9082
|
uniqueDocuments: import_zod.z.boolean().optional().describe("When true, return at most one chunk per source document."),
|
|
9083
|
+
includeContext: import_zod.z.boolean().optional().describe(
|
|
9084
|
+
"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."
|
|
9085
|
+
),
|
|
9047
9086
|
requireComplete: import_zod.z.boolean().optional().describe(
|
|
9048
9087
|
"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)."
|
|
9049
9088
|
)
|
|
@@ -9051,7 +9090,7 @@ var inputSchema = {
|
|
|
9051
9090
|
var hybridSearch = ({ client, log }) => ({
|
|
9052
9091
|
name: "hybrid_search",
|
|
9053
9092
|
title: "Hybrid search",
|
|
9054
|
-
description:
|
|
9093
|
+
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".`,
|
|
9055
9094
|
inputSchema,
|
|
9056
9095
|
handler: async (args) => {
|
|
9057
9096
|
const limit = args.limit ?? MCP_DEFAULT_LIMIT;
|
|
@@ -9078,13 +9117,30 @@ var hybridSearch = ({ client, log }) => ({
|
|
|
9078
9117
|
uniqueDocuments: args.uniqueDocuments,
|
|
9079
9118
|
requireComplete: args.requireComplete
|
|
9080
9119
|
});
|
|
9120
|
+
const hits = Array.isArray(
|
|
9121
|
+
result.results
|
|
9122
|
+
) ? result.results : [];
|
|
9123
|
+
const includeContext = args.includeContext ?? false;
|
|
9124
|
+
let shaped = hits.map((h) => reshapeHit(h, includeContext));
|
|
9125
|
+
const textLegEmpty = mode !== "SEMANTIC" && hits.length > 0 && hits.every((h) => !(Number(h.textScore) > 0));
|
|
9126
|
+
const envelope = {
|
|
9127
|
+
...result,
|
|
9128
|
+
results: shaped
|
|
9129
|
+
};
|
|
9130
|
+
if (textLegEmpty) envelope.textLegEmpty = true;
|
|
9131
|
+
let text = JSON.stringify(envelope, null, 2);
|
|
9132
|
+
while (shaped.length > 1 && text.length > MCP_RESPONSE_CHAR_BUDGET) {
|
|
9133
|
+
shaped = shaped.slice(0, -1);
|
|
9134
|
+
envelope.results = shaped;
|
|
9135
|
+
envelope.truncated = true;
|
|
9136
|
+
envelope.returnedResults = shaped.length;
|
|
9137
|
+
text = JSON.stringify(envelope, null, 2);
|
|
9138
|
+
}
|
|
9081
9139
|
log.debug(
|
|
9082
|
-
{ tool: "hybrid_search", mode, limit, returned:
|
|
9140
|
+
{ tool: "hybrid_search", mode, limit, returned: shaped.length, textLegEmpty },
|
|
9083
9141
|
"hybrid_search ok"
|
|
9084
9142
|
);
|
|
9085
|
-
return {
|
|
9086
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
9087
|
-
};
|
|
9143
|
+
return { content: [{ type: "text", text }] };
|
|
9088
9144
|
} catch (err) {
|
|
9089
9145
|
log.warn({ tool: "hybrid_search", err: String(err) }, "hybrid_search failed");
|
|
9090
9146
|
return toolError("hybrid_search", err);
|
|
@@ -9119,7 +9175,9 @@ var MCP_MAX_LIMIT2 = 10;
|
|
|
9119
9175
|
var inputSchema2 = {
|
|
9120
9176
|
type: import_zod2.z.string().min(1, "type (record type) is required").describe('Record type / schema name (e.g. "patient", "clinical_note").'),
|
|
9121
9177
|
// Lookup-mode args — provide `field` plus EXACTLY ONE of: value | from+to | prefix.
|
|
9122
|
-
field: import_zod2.z.string().optional().describe(
|
|
9178
|
+
field: import_zod2.z.string().optional().describe(
|
|
9179
|
+
'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.)'
|
|
9180
|
+
),
|
|
9123
9181
|
value: import_zod2.z.string().optional().describe("Lookup mode (equality): exact-match value for `field`."),
|
|
9124
9182
|
from: import_zod2.z.string().optional().describe("Lookup mode (range): inclusive lower bound; requires `to`. Non-sensitive fields only."),
|
|
9125
9183
|
to: import_zod2.z.string().optional().describe("Lookup mode (range): inclusive upper bound; requires `from`."),
|
|
@@ -9214,19 +9272,77 @@ var record_query_default = recordQuery;
|
|
|
9214
9272
|
|
|
9215
9273
|
// src/tools/record_get.ts
|
|
9216
9274
|
var import_zod3 = require("zod");
|
|
9275
|
+
|
|
9276
|
+
// src/tools/resolve-external-id.ts
|
|
9277
|
+
function validateSelector(sel, entity) {
|
|
9278
|
+
const hasId = sel.id !== void 0 && sel.id !== "";
|
|
9279
|
+
const hasExt = sel.externalId !== void 0 && sel.externalId !== "";
|
|
9280
|
+
if (hasId && hasExt) {
|
|
9281
|
+
return `Provide either id or externalId (not both) to identify the ${entity}.`;
|
|
9282
|
+
}
|
|
9283
|
+
if (!hasId && !hasExt) {
|
|
9284
|
+
return `Provide id, or externalId + type, to identify the ${entity}.`;
|
|
9285
|
+
}
|
|
9286
|
+
if (hasExt && (sel.type === void 0 || sel.type === "")) {
|
|
9287
|
+
return "type is required when selecting by externalId (externalId is unique within a type).";
|
|
9288
|
+
}
|
|
9289
|
+
return void 0;
|
|
9290
|
+
}
|
|
9291
|
+
async function resolveRecordIdByExternalId(client, sel) {
|
|
9292
|
+
const err = validateSelector(sel, "record");
|
|
9293
|
+
if (err) return { error: err };
|
|
9294
|
+
if (sel.id) return { id: sel.id };
|
|
9295
|
+
const page = await client.records.lookupRecordsByBody({
|
|
9296
|
+
type: sel.type,
|
|
9297
|
+
field: "externalId",
|
|
9298
|
+
value: sel.externalId,
|
|
9299
|
+
limit: 1
|
|
9300
|
+
});
|
|
9301
|
+
const items = pageItems(page);
|
|
9302
|
+
if (items.length === 0) {
|
|
9303
|
+
return { error: `No record of type '${sel.type}' with externalId '${sel.externalId}'.` };
|
|
9304
|
+
}
|
|
9305
|
+
return { id: items[0].id };
|
|
9306
|
+
}
|
|
9307
|
+
async function resolveDocumentIdByExternalId(client, sel) {
|
|
9308
|
+
const err = validateSelector(sel, "document");
|
|
9309
|
+
if (err) return { error: err };
|
|
9310
|
+
if (sel.id) return { id: sel.id };
|
|
9311
|
+
const page = await client.documents.lookupDocumentsByBody({
|
|
9312
|
+
type: sel.type,
|
|
9313
|
+
field: "externalId",
|
|
9314
|
+
value: sel.externalId,
|
|
9315
|
+
limit: 1
|
|
9316
|
+
});
|
|
9317
|
+
const items = pageItems(page);
|
|
9318
|
+
if (items.length === 0) {
|
|
9319
|
+
return { error: `No document of type '${sel.type}' with externalId '${sel.externalId}'.` };
|
|
9320
|
+
}
|
|
9321
|
+
return { id: items[0].id };
|
|
9322
|
+
}
|
|
9323
|
+
|
|
9324
|
+
// src/tools/record_get.ts
|
|
9217
9325
|
var MAX_PAYLOAD_CHARS = 32e3;
|
|
9218
9326
|
var inputSchema3 = {
|
|
9219
|
-
id: import_zod3.z.string().
|
|
9327
|
+
id: import_zod3.z.string().optional().describe("The Vectros record id (e.g. from record_query or hybrid_search). Provide this OR externalId+type."),
|
|
9328
|
+
externalId: import_zod3.z.string().optional().describe(
|
|
9329
|
+
"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."
|
|
9330
|
+
),
|
|
9331
|
+
type: import_zod3.z.string().optional().describe("Record type \u2014 REQUIRED when selecting by `externalId` (externalId is unique within a type).")
|
|
9220
9332
|
};
|
|
9221
9333
|
var recordGet = ({ client, log }) => ({
|
|
9222
9334
|
name: "record_get",
|
|
9223
|
-
title: "Get a record by id",
|
|
9224
|
-
description: "Fetch a single structured record
|
|
9335
|
+
title: "Get a record by id or externalId",
|
|
9336
|
+
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.",
|
|
9225
9337
|
inputSchema: inputSchema3,
|
|
9226
9338
|
handler: async (args) => {
|
|
9227
9339
|
const id = args.id;
|
|
9340
|
+
const externalId = args.externalId;
|
|
9341
|
+
const type = args.type;
|
|
9228
9342
|
try {
|
|
9229
|
-
const
|
|
9343
|
+
const resolved = await resolveRecordIdByExternalId(client, { id, externalId, type });
|
|
9344
|
+
if ("error" in resolved) return toolError("record_get", new Error(resolved.error));
|
|
9345
|
+
const record = await client.records.getRecord({ id: resolved.id });
|
|
9230
9346
|
const out = { ...record };
|
|
9231
9347
|
if (record.payload !== void 0) {
|
|
9232
9348
|
const json = JSON.stringify(record.payload);
|
|
@@ -9238,12 +9354,12 @@ var recordGet = ({ client, log }) => ({
|
|
|
9238
9354
|
}
|
|
9239
9355
|
}
|
|
9240
9356
|
log.debug(
|
|
9241
|
-
{ tool: "record_get", id, truncated: out.payloadTruncated === true },
|
|
9357
|
+
{ tool: "record_get", id: resolved.id, externalId, truncated: out.payloadTruncated === true },
|
|
9242
9358
|
"record_get ok"
|
|
9243
9359
|
);
|
|
9244
9360
|
return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
|
|
9245
9361
|
} catch (err) {
|
|
9246
|
-
log.warn({ tool: "record_get", id, err: String(err) }, "record_get failed");
|
|
9362
|
+
log.warn({ tool: "record_get", id, externalId, type, err: String(err) }, "record_get failed");
|
|
9247
9363
|
return toolError("record_get", err);
|
|
9248
9364
|
}
|
|
9249
9365
|
}
|
|
@@ -9303,7 +9419,11 @@ var record_create_default = recordCreate;
|
|
|
9303
9419
|
// src/tools/record_update.ts
|
|
9304
9420
|
var import_zod5 = require("zod");
|
|
9305
9421
|
var inputSchema5 = {
|
|
9306
|
-
id: import_zod5.z.string().
|
|
9422
|
+
id: import_zod5.z.string().optional().describe("The Vectros record id to update. Provide this OR externalId+type."),
|
|
9423
|
+
externalId: import_zod5.z.string().optional().describe(
|
|
9424
|
+
"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."
|
|
9425
|
+
),
|
|
9426
|
+
type: import_zod5.z.string().optional().describe("Record type \u2014 REQUIRED when selecting by `externalId` (externalId is unique within a type)."),
|
|
9307
9427
|
fields: import_zod5.z.record(import_zod5.z.string(), import_zod5.z.unknown()).describe(
|
|
9308
9428
|
"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."
|
|
9309
9429
|
),
|
|
@@ -9314,23 +9434,27 @@ var inputSchema5 = {
|
|
|
9314
9434
|
};
|
|
9315
9435
|
var recordUpdate = ({ client, log }) => ({
|
|
9316
9436
|
name: "record_update",
|
|
9317
|
-
title: "Update a record",
|
|
9318
|
-
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.",
|
|
9437
|
+
title: "Update a record by id or externalId",
|
|
9438
|
+
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.",
|
|
9319
9439
|
inputSchema: inputSchema5,
|
|
9320
9440
|
handler: async (args) => {
|
|
9321
9441
|
const id = args.id;
|
|
9442
|
+
const externalId = args.externalId;
|
|
9443
|
+
const type = args.type;
|
|
9322
9444
|
const fields = args.fields;
|
|
9323
9445
|
const status = args.status;
|
|
9324
9446
|
const expectedVersion = args.expectedVersion;
|
|
9325
9447
|
try {
|
|
9448
|
+
const resolved = await resolveRecordIdByExternalId(client, { id, externalId, type });
|
|
9449
|
+
if ("error" in resolved) return toolError("record_update", new Error(resolved.error));
|
|
9326
9450
|
const body = { payload: fields };
|
|
9327
9451
|
if (status !== void 0) body.status = status;
|
|
9328
9452
|
if (expectedVersion !== void 0) body.expectedVersion = expectedVersion;
|
|
9329
|
-
const updated = await client.records.patchRecord({ id, body });
|
|
9330
|
-
log.debug({ tool: "record_update", id }, "record_update ok");
|
|
9453
|
+
const updated = await client.records.patchRecord({ id: resolved.id, body });
|
|
9454
|
+
log.debug({ tool: "record_update", id: resolved.id, externalId }, "record_update ok");
|
|
9331
9455
|
return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
|
|
9332
9456
|
} catch (err) {
|
|
9333
|
-
log.warn({ tool: "record_update", id, err: String(err) }, "record_update failed");
|
|
9457
|
+
log.warn({ tool: "record_update", id, externalId, type, err: String(err) }, "record_update failed");
|
|
9334
9458
|
return toolError("record_update", err);
|
|
9335
9459
|
}
|
|
9336
9460
|
}
|
|
@@ -9597,7 +9721,11 @@ var list_schemas_default = listSchemas;
|
|
|
9597
9721
|
var import_zod10 = require("zod");
|
|
9598
9722
|
var MAX_TEXT_CHARS = 32e3;
|
|
9599
9723
|
var inputSchema10 = {
|
|
9600
|
-
documentId: import_zod10.z.string().
|
|
9724
|
+
documentId: import_zod10.z.string().optional().describe("The document ID returned by document_ingest or hybrid_search. Provide this OR externalId+type."),
|
|
9725
|
+
externalId: import_zod10.z.string().optional().describe(
|
|
9726
|
+
"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."
|
|
9727
|
+
),
|
|
9728
|
+
type: import_zod10.z.string().optional().describe("Document type \u2014 REQUIRED when selecting by `externalId` (externalId is unique within a type)."),
|
|
9601
9729
|
includeText: import_zod10.z.boolean().optional().describe(
|
|
9602
9730
|
"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."
|
|
9603
9731
|
),
|
|
@@ -9617,13 +9745,18 @@ function isNotAvailable(err) {
|
|
|
9617
9745
|
var documentGet = ({ client, log }) => ({
|
|
9618
9746
|
name: "document_get",
|
|
9619
9747
|
title: "Get document metadata (and optionally text)",
|
|
9620
|
-
description: "Fetch metadata for a single document by
|
|
9748
|
+
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`.",
|
|
9621
9749
|
inputSchema: inputSchema10,
|
|
9622
9750
|
handler: async (args) => {
|
|
9623
|
-
const
|
|
9751
|
+
const documentId = args.documentId;
|
|
9752
|
+
const externalId = args.externalId;
|
|
9753
|
+
const type = args.type;
|
|
9624
9754
|
const includeText = args.includeText ?? false;
|
|
9625
9755
|
const includeDownloadUrl = args.includeDownloadUrl ?? false;
|
|
9626
9756
|
try {
|
|
9757
|
+
const resolved = await resolveDocumentIdByExternalId(client, { id: documentId, externalId, type });
|
|
9758
|
+
if ("error" in resolved) return toolError("document_get", new Error(resolved.error));
|
|
9759
|
+
const id = resolved.id;
|
|
9627
9760
|
const doc = await client.documents.getDocument({ id });
|
|
9628
9761
|
let extra = {};
|
|
9629
9762
|
if (includeText) {
|
|
@@ -9670,7 +9803,7 @@ var documentGet = ({ client, log }) => ({
|
|
|
9670
9803
|
]
|
|
9671
9804
|
};
|
|
9672
9805
|
} catch (err) {
|
|
9673
|
-
log.warn({ tool: "document_get",
|
|
9806
|
+
log.warn({ tool: "document_get", documentId, externalId, type, err: String(err) }, "document_get failed");
|
|
9674
9807
|
return toolError("document_get", err);
|
|
9675
9808
|
}
|
|
9676
9809
|
}
|
|
@@ -9785,13 +9918,23 @@ var document_query_default = documentQuery;
|
|
|
9785
9918
|
// src/tools/document_update.ts
|
|
9786
9919
|
var import_zod12 = require("zod");
|
|
9787
9920
|
var inputSchema12 = {
|
|
9788
|
-
documentId: import_zod12.z.string().
|
|
9921
|
+
documentId: import_zod12.z.string().optional().describe("The Vectros document id to update. Provide this OR externalId+type."),
|
|
9922
|
+
externalId: import_zod12.z.string().optional().describe(
|
|
9923
|
+
"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."
|
|
9924
|
+
),
|
|
9925
|
+
type: import_zod12.z.string().optional().describe("Document type \u2014 REQUIRED when selecting by `externalId` (externalId is unique within a type)."),
|
|
9789
9926
|
title: import_zod12.z.string().optional().describe("New title. Omit to keep the current title."),
|
|
9790
9927
|
fields: import_zod12.z.record(import_zod12.z.string(), import_zod12.z.unknown()).optional().describe(
|
|
9791
9928
|
"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."
|
|
9792
9929
|
),
|
|
9930
|
+
text: import_zod12.z.string().optional().describe(
|
|
9931
|
+
"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."
|
|
9932
|
+
),
|
|
9793
9933
|
folderId: import_zod12.z.string().optional().describe("Move the document into this folder (cannot be cleared once set)."),
|
|
9794
9934
|
storeText: import_zod12.z.boolean().optional().describe("Whether the raw text is stored + retrievable."),
|
|
9935
|
+
status: import_zod12.z.enum(["ACTIVE", "ARCHIVED"]).optional().describe(
|
|
9936
|
+
"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."
|
|
9937
|
+
),
|
|
9795
9938
|
userId: import_zod12.z.string().optional().describe("Reassign the owning user (Vectros UUID)."),
|
|
9796
9939
|
orgId: import_zod12.z.string().optional().describe("Reassign the owning org (Vectros UUID)."),
|
|
9797
9940
|
clientId: import_zod12.z.string().optional().describe("Reassign the associated client (Vectros UUID)."),
|
|
@@ -9802,33 +9945,41 @@ var inputSchema12 = {
|
|
|
9802
9945
|
var documentUpdate = ({ client, log }) => ({
|
|
9803
9946
|
name: "document_update",
|
|
9804
9947
|
title: "Update a document",
|
|
9805
|
-
description:
|
|
9948
|
+
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.',
|
|
9806
9949
|
inputSchema: inputSchema12,
|
|
9807
9950
|
handler: async (args) => {
|
|
9808
9951
|
const documentId = args.documentId;
|
|
9952
|
+
const externalId = args.externalId;
|
|
9953
|
+
const type = args.type;
|
|
9809
9954
|
const title = args.title;
|
|
9810
9955
|
const fields = args.fields;
|
|
9956
|
+
const text = args.text;
|
|
9811
9957
|
const folderId = args.folderId;
|
|
9812
9958
|
const storeText = args.storeText;
|
|
9959
|
+
const status = args.status;
|
|
9813
9960
|
const userId = args.userId;
|
|
9814
9961
|
const orgId = args.orgId;
|
|
9815
9962
|
const clientId = args.clientId;
|
|
9816
9963
|
const expectedVersion = args.expectedVersion;
|
|
9817
9964
|
try {
|
|
9965
|
+
const resolved = await resolveDocumentIdByExternalId(client, { id: documentId, externalId, type });
|
|
9966
|
+
if ("error" in resolved) return toolError("document_update", new Error(resolved.error));
|
|
9818
9967
|
const body = {};
|
|
9819
9968
|
if (title !== void 0) body.title = title;
|
|
9820
9969
|
if (fields !== void 0) body.payload = fields;
|
|
9970
|
+
if (text !== void 0) body.text = text;
|
|
9821
9971
|
if (folderId !== void 0) body.folderId = folderId;
|
|
9822
9972
|
if (storeText !== void 0) body.storeText = storeText;
|
|
9973
|
+
if (status !== void 0) body.status = status;
|
|
9823
9974
|
if (userId !== void 0) body.userId = userId;
|
|
9824
9975
|
if (orgId !== void 0) body.orgId = orgId;
|
|
9825
9976
|
if (clientId !== void 0) body.clientId = clientId;
|
|
9826
9977
|
if (expectedVersion !== void 0) body.expectedVersion = expectedVersion;
|
|
9827
9978
|
const updated = await client.documents.patchDocument({
|
|
9828
|
-
id:
|
|
9979
|
+
id: resolved.id,
|
|
9829
9980
|
body
|
|
9830
9981
|
});
|
|
9831
|
-
log.debug({ tool: "document_update", documentId }, "document_update ok");
|
|
9982
|
+
log.debug({ tool: "document_update", documentId: resolved.id, externalId }, "document_update ok");
|
|
9832
9983
|
return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
|
|
9833
9984
|
} catch (err) {
|
|
9834
9985
|
log.warn({ tool: "document_update", documentId, err: String(err) }, "document_update failed");
|
|
@@ -9846,7 +9997,7 @@ var inputSchema13 = {
|
|
|
9846
9997
|
var documentDelete = ({ client, log }) => ({
|
|
9847
9998
|
name: "document_delete",
|
|
9848
9999
|
title: "Delete a document",
|
|
9849
|
-
description:
|
|
10000
|
+
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).',
|
|
9850
10001
|
inputSchema: inputSchema13,
|
|
9851
10002
|
handler: async (args) => {
|
|
9852
10003
|
const documentId = args.documentId;
|
|
@@ -10126,6 +10277,9 @@ var inputSchema19 = {
|
|
|
10126
10277
|
externalId: import_zod18.z.string().optional().describe(
|
|
10127
10278
|
"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."
|
|
10128
10279
|
),
|
|
10280
|
+
upsert: import_zod18.z.boolean().optional().describe(
|
|
10281
|
+
"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)."
|
|
10282
|
+
),
|
|
10129
10283
|
schemaId: import_zod18.z.string().optional().describe(
|
|
10130
10284
|
"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."
|
|
10131
10285
|
),
|
|
@@ -10151,13 +10305,14 @@ var inputSchema19 = {
|
|
|
10151
10305
|
var documentIngest = ({ client, log, transport, ingestRoot }) => ({
|
|
10152
10306
|
name: "document_ingest",
|
|
10153
10307
|
title: "Ingest a new document (text body or file upload)",
|
|
10154
|
-
description: "Create a new document in
|
|
10308
|
+
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).",
|
|
10155
10309
|
inputSchema: inputSchema19,
|
|
10156
10310
|
handler: async (args) => {
|
|
10157
10311
|
const title = args.title;
|
|
10158
10312
|
const text = args.text;
|
|
10159
10313
|
const filePath = args.filePath;
|
|
10160
10314
|
const schemaId = args.schemaId;
|
|
10315
|
+
const upsert = args.upsert;
|
|
10161
10316
|
const indexMode = args.indexMode ?? (schemaId ? void 0 : "HYBRID");
|
|
10162
10317
|
if (!text && !filePath) {
|
|
10163
10318
|
return toolError(
|
|
@@ -10192,6 +10347,7 @@ var documentIngest = ({ client, log, transport, ingestRoot }) => ({
|
|
|
10192
10347
|
if (text) {
|
|
10193
10348
|
const storeText = args.storeText ?? true;
|
|
10194
10349
|
const result = await client.documents.ingestDocument({
|
|
10350
|
+
upsert,
|
|
10195
10351
|
body: {
|
|
10196
10352
|
title,
|
|
10197
10353
|
text,
|
|
@@ -10228,6 +10384,7 @@ var documentIngest = ({ client, log, transport, ingestRoot }) => ({
|
|
|
10228
10384
|
const fileName = (0, import_node_path.basename)(safePath);
|
|
10229
10385
|
const fileType = args.fileType ?? inferMimeType(safePath);
|
|
10230
10386
|
const upload = await client.documents.uploadDocument({
|
|
10387
|
+
upsert,
|
|
10231
10388
|
fileName,
|
|
10232
10389
|
fileType,
|
|
10233
10390
|
indexMode,
|
|
@@ -10266,8 +10423,8 @@ var documentIngest = ({ client, log, transport, ingestRoot }) => ({
|
|
|
10266
10423
|
text: JSON.stringify(
|
|
10267
10424
|
{
|
|
10268
10425
|
...upload,
|
|
10269
|
-
|
|
10270
|
-
_note: "File uploaded; indexing is asynchronous. Poll document_get(id) until
|
|
10426
|
+
indexStatus: "PENDING_INDEX",
|
|
10427
|
+
_note: "File uploaded; indexing is asynchronous. Poll document_get(id) until indexStatus is INDEXED."
|
|
10271
10428
|
},
|
|
10272
10429
|
null,
|
|
10273
10430
|
2
|
|
@@ -10564,6 +10721,14 @@ function zodTypeToJsonSchema(schema) {
|
|
|
10564
10721
|
|
|
10565
10722
|
// src/server.ts
|
|
10566
10723
|
var DEFAULT_ENVIRONMENT = "https://api.vectros.ai";
|
|
10724
|
+
function formatArgError(tool, error) {
|
|
10725
|
+
const unknownKeys = error.issues.filter((i) => i.code === "unrecognized_keys").flatMap((i) => i.keys);
|
|
10726
|
+
if (unknownKeys.length > 0) {
|
|
10727
|
+
const valid = Object.keys(tool.inputSchema).join(", ");
|
|
10728
|
+
return `Unknown argument(s): ${unknownKeys.join(", ")}. Valid arguments for ${tool.name}: ${valid}. Pass only these \u2014 an unrecognized argument is rejected, not ignored.`;
|
|
10729
|
+
}
|
|
10730
|
+
return `Invalid arguments: ${error.message}`;
|
|
10731
|
+
}
|
|
10567
10732
|
var VectrosMCPServer = class {
|
|
10568
10733
|
server;
|
|
10569
10734
|
log;
|
|
@@ -10653,7 +10818,11 @@ var VectrosMCPServer = class {
|
|
|
10653
10818
|
name: t.name,
|
|
10654
10819
|
title: t.title,
|
|
10655
10820
|
description: t.description,
|
|
10656
|
-
|
|
10821
|
+
// additionalProperties:false advertises strict argument validation to the
|
|
10822
|
+
// MCP client (a well-behaved client rejects an unknown key before the call).
|
|
10823
|
+
// The server enforces it too (z.object(...).strict() below) so a raw JSON-RPC
|
|
10824
|
+
// call that skips client validation is still rejected — belt and suspenders.
|
|
10825
|
+
inputSchema: { ...zodShapeToJsonSchema(t.inputSchema), additionalProperties: false }
|
|
10657
10826
|
}))
|
|
10658
10827
|
})
|
|
10659
10828
|
);
|
|
@@ -10663,9 +10832,9 @@ var VectrosMCPServer = class {
|
|
|
10663
10832
|
if (!tool) {
|
|
10664
10833
|
result = toolError(req.params.name ?? "unknown", new Error("No such tool."));
|
|
10665
10834
|
} else {
|
|
10666
|
-
const parsed = import_zod22.z.object(tool.inputSchema).safeParse(req.params.arguments ?? {});
|
|
10835
|
+
const parsed = import_zod22.z.object(tool.inputSchema).strict().safeParse(req.params.arguments ?? {});
|
|
10667
10836
|
if (!parsed.success) {
|
|
10668
|
-
result = toolError(tool.name, new Error(
|
|
10837
|
+
result = toolError(tool.name, new Error(formatArgError(tool, parsed.error)));
|
|
10669
10838
|
} else {
|
|
10670
10839
|
try {
|
|
10671
10840
|
result = await tool.handler(
|