@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/index.js
CHANGED
|
@@ -520,10 +520,18 @@ var DocumentRequest;
|
|
|
520
520
|
Text: "TEXT",
|
|
521
521
|
None: "NONE"
|
|
522
522
|
};
|
|
523
|
+
DocumentRequest2.Status = {
|
|
524
|
+
Active: "ACTIVE",
|
|
525
|
+
Archived: "ARCHIVED"
|
|
526
|
+
};
|
|
523
527
|
})(DocumentRequest || (DocumentRequest = {}));
|
|
524
528
|
var DocumentResponse;
|
|
525
529
|
((DocumentResponse2) => {
|
|
526
530
|
DocumentResponse2.Status = {
|
|
531
|
+
Active: "ACTIVE",
|
|
532
|
+
Archived: "ARCHIVED"
|
|
533
|
+
};
|
|
534
|
+
DocumentResponse2.IndexStatus = {
|
|
527
535
|
PendingUpload: "PENDING_UPLOAD",
|
|
528
536
|
Uploaded: "UPLOADED",
|
|
529
537
|
Extracting: "EXTRACTING",
|
|
@@ -5294,7 +5302,7 @@ var DocumentsClient = class {
|
|
|
5294
5302
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/documents/{id}/versions");
|
|
5295
5303
|
}
|
|
5296
5304
|
/**
|
|
5297
|
-
* 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
|
|
5305
|
+
* 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).
|
|
5298
5306
|
*
|
|
5299
5307
|
* @param {Vectros.FileUploadRequest} request
|
|
5300
5308
|
* @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -8790,8 +8798,8 @@ function warnOnSuboptimalKey(key, log) {
|
|
|
8790
8798
|
|
|
8791
8799
|
// src/build-info.ts
|
|
8792
8800
|
var BUILD_INFO = {
|
|
8793
|
-
mcpServer: true ? "0.
|
|
8794
|
-
sdk: true ? "0.
|
|
8801
|
+
mcpServer: true ? "0.6.0" : "dev",
|
|
8802
|
+
sdk: true ? "0.32.0" : "dev"
|
|
8795
8803
|
};
|
|
8796
8804
|
|
|
8797
8805
|
// src/log.ts
|
|
@@ -9028,6 +9036,32 @@ ${detail}${pointer}`
|
|
|
9028
9036
|
// src/tools/hybrid_search.ts
|
|
9029
9037
|
var MCP_DEFAULT_LIMIT = 3;
|
|
9030
9038
|
var MCP_MAX_LIMIT = 10;
|
|
9039
|
+
var MCP_RESPONSE_CHAR_BUDGET = 24e3;
|
|
9040
|
+
var SNIPPET_MAX_CHARS = 160;
|
|
9041
|
+
var INTERNAL_METADATA_KEYS = /* @__PURE__ */ new Set(["tenantId", "owner_id", "model_type", "rootFolderId", "folderId"]);
|
|
9042
|
+
function trimMetadata(md) {
|
|
9043
|
+
if (!md || typeof md !== "object" || Array.isArray(md)) return md;
|
|
9044
|
+
const out = {};
|
|
9045
|
+
for (const [k, v] of Object.entries(md)) {
|
|
9046
|
+
if (!INTERNAL_METADATA_KEYS.has(k)) out[k] = v;
|
|
9047
|
+
}
|
|
9048
|
+
return out;
|
|
9049
|
+
}
|
|
9050
|
+
function reshapeHit(hit, includeContext) {
|
|
9051
|
+
const out = { ...hit };
|
|
9052
|
+
if ("metadata" in out) out.metadata = trimMetadata(out.metadata);
|
|
9053
|
+
const chunk = typeof hit.chunkText === "string" ? hit.chunkText : void 0;
|
|
9054
|
+
const ctx = typeof hit.contextText === "string" ? hit.contextText : void 0;
|
|
9055
|
+
if (includeContext) {
|
|
9056
|
+
if (chunk && ctx && ctx.includes(chunk)) delete out.chunkText;
|
|
9057
|
+
} else {
|
|
9058
|
+
delete out.contextText;
|
|
9059
|
+
}
|
|
9060
|
+
if ((out.snippet == null || out.snippet === "") && chunk) {
|
|
9061
|
+
out.snippet = chunk.length > SNIPPET_MAX_CHARS ? chunk.slice(0, SNIPPET_MAX_CHARS) + "\u2026" : chunk;
|
|
9062
|
+
}
|
|
9063
|
+
return out;
|
|
9064
|
+
}
|
|
9031
9065
|
var inputSchema = {
|
|
9032
9066
|
query: import_zod.z.string().min(1, "query must be non-empty").describe("Natural-language or keyword query."),
|
|
9033
9067
|
mode: import_zod.z.enum(["HYBRID", "TEXT", "SEMANTIC"]).optional().describe("HYBRID (BM25 + dense, default), TEXT (BM25 only), or SEMANTIC (dense only)."),
|
|
@@ -9042,7 +9076,9 @@ var inputSchema = {
|
|
|
9042
9076
|
rootFolderId: import_zod.z.string().optional().describe("Scope to content under this folder AND all its descendants (subtree-root folder id)."),
|
|
9043
9077
|
// Content narrowing.
|
|
9044
9078
|
contentTypes: import_zod.z.array(import_zod.z.enum(["documents", "records"])).optional().describe('Narrow to content types. ["documents"] or ["records"]; omit for unified (both).'),
|
|
9045
|
-
typeName: import_zod.z.string().optional().describe(
|
|
9079
|
+
typeName: import_zod.z.string().optional().describe(
|
|
9080
|
+
'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.'
|
|
9081
|
+
),
|
|
9046
9082
|
filters: import_zod.z.record(import_zod.z.unknown()).optional().describe(
|
|
9047
9083
|
'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}}.'
|
|
9048
9084
|
),
|
|
@@ -9057,6 +9093,9 @@ var inputSchema = {
|
|
|
9057
9093
|
minSimilarity: import_zod.z.number().min(0).max(1).optional().describe("Minimum semantic similarity (0.0\u20131.0); results below are excluded."),
|
|
9058
9094
|
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."),
|
|
9059
9095
|
uniqueDocuments: import_zod.z.boolean().optional().describe("When true, return at most one chunk per source document."),
|
|
9096
|
+
includeContext: import_zod.z.boolean().optional().describe(
|
|
9097
|
+
"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."
|
|
9098
|
+
),
|
|
9060
9099
|
requireComplete: import_zod.z.boolean().optional().describe(
|
|
9061
9100
|
"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)."
|
|
9062
9101
|
)
|
|
@@ -9064,7 +9103,7 @@ var inputSchema = {
|
|
|
9064
9103
|
var hybridSearch = ({ client, log }) => ({
|
|
9065
9104
|
name: "hybrid_search",
|
|
9066
9105
|
title: "Hybrid search",
|
|
9067
|
-
description:
|
|
9106
|
+
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".`,
|
|
9068
9107
|
inputSchema,
|
|
9069
9108
|
handler: async (args) => {
|
|
9070
9109
|
const limit = args.limit ?? MCP_DEFAULT_LIMIT;
|
|
@@ -9091,13 +9130,30 @@ var hybridSearch = ({ client, log }) => ({
|
|
|
9091
9130
|
uniqueDocuments: args.uniqueDocuments,
|
|
9092
9131
|
requireComplete: args.requireComplete
|
|
9093
9132
|
});
|
|
9133
|
+
const hits = Array.isArray(
|
|
9134
|
+
result.results
|
|
9135
|
+
) ? result.results : [];
|
|
9136
|
+
const includeContext = args.includeContext ?? false;
|
|
9137
|
+
let shaped = hits.map((h) => reshapeHit(h, includeContext));
|
|
9138
|
+
const textLegEmpty = mode !== "SEMANTIC" && hits.length > 0 && hits.every((h) => !(Number(h.textScore) > 0));
|
|
9139
|
+
const envelope = {
|
|
9140
|
+
...result,
|
|
9141
|
+
results: shaped
|
|
9142
|
+
};
|
|
9143
|
+
if (textLegEmpty) envelope.textLegEmpty = true;
|
|
9144
|
+
let text = JSON.stringify(envelope, null, 2);
|
|
9145
|
+
while (shaped.length > 1 && text.length > MCP_RESPONSE_CHAR_BUDGET) {
|
|
9146
|
+
shaped = shaped.slice(0, -1);
|
|
9147
|
+
envelope.results = shaped;
|
|
9148
|
+
envelope.truncated = true;
|
|
9149
|
+
envelope.returnedResults = shaped.length;
|
|
9150
|
+
text = JSON.stringify(envelope, null, 2);
|
|
9151
|
+
}
|
|
9094
9152
|
log.debug(
|
|
9095
|
-
{ tool: "hybrid_search", mode, limit, returned:
|
|
9153
|
+
{ tool: "hybrid_search", mode, limit, returned: shaped.length, textLegEmpty },
|
|
9096
9154
|
"hybrid_search ok"
|
|
9097
9155
|
);
|
|
9098
|
-
return {
|
|
9099
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
9100
|
-
};
|
|
9156
|
+
return { content: [{ type: "text", text }] };
|
|
9101
9157
|
} catch (err) {
|
|
9102
9158
|
log.warn({ tool: "hybrid_search", err: String(err) }, "hybrid_search failed");
|
|
9103
9159
|
return toolError("hybrid_search", err);
|
|
@@ -9132,7 +9188,9 @@ var MCP_MAX_LIMIT2 = 10;
|
|
|
9132
9188
|
var inputSchema2 = {
|
|
9133
9189
|
type: import_zod2.z.string().min(1, "type (record type) is required").describe('Record type / schema name (e.g. "patient", "clinical_note").'),
|
|
9134
9190
|
// Lookup-mode args — provide `field` plus EXACTLY ONE of: value | from+to | prefix.
|
|
9135
|
-
field: import_zod2.z.string().optional().describe(
|
|
9191
|
+
field: import_zod2.z.string().optional().describe(
|
|
9192
|
+
'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.)'
|
|
9193
|
+
),
|
|
9136
9194
|
value: import_zod2.z.string().optional().describe("Lookup mode (equality): exact-match value for `field`."),
|
|
9137
9195
|
from: import_zod2.z.string().optional().describe("Lookup mode (range): inclusive lower bound; requires `to`. Non-sensitive fields only."),
|
|
9138
9196
|
to: import_zod2.z.string().optional().describe("Lookup mode (range): inclusive upper bound; requires `from`."),
|
|
@@ -9227,19 +9285,77 @@ var record_query_default = recordQuery;
|
|
|
9227
9285
|
|
|
9228
9286
|
// src/tools/record_get.ts
|
|
9229
9287
|
var import_zod3 = require("zod");
|
|
9288
|
+
|
|
9289
|
+
// src/tools/resolve-external-id.ts
|
|
9290
|
+
function validateSelector(sel, entity) {
|
|
9291
|
+
const hasId = sel.id !== void 0 && sel.id !== "";
|
|
9292
|
+
const hasExt = sel.externalId !== void 0 && sel.externalId !== "";
|
|
9293
|
+
if (hasId && hasExt) {
|
|
9294
|
+
return `Provide either id or externalId (not both) to identify the ${entity}.`;
|
|
9295
|
+
}
|
|
9296
|
+
if (!hasId && !hasExt) {
|
|
9297
|
+
return `Provide id, or externalId + type, to identify the ${entity}.`;
|
|
9298
|
+
}
|
|
9299
|
+
if (hasExt && (sel.type === void 0 || sel.type === "")) {
|
|
9300
|
+
return "type is required when selecting by externalId (externalId is unique within a type).";
|
|
9301
|
+
}
|
|
9302
|
+
return void 0;
|
|
9303
|
+
}
|
|
9304
|
+
async function resolveRecordIdByExternalId(client, sel) {
|
|
9305
|
+
const err = validateSelector(sel, "record");
|
|
9306
|
+
if (err) return { error: err };
|
|
9307
|
+
if (sel.id) return { id: sel.id };
|
|
9308
|
+
const page = await client.records.lookupRecordsByBody({
|
|
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 record of type '${sel.type}' with externalId '${sel.externalId}'.` };
|
|
9317
|
+
}
|
|
9318
|
+
return { id: items[0].id };
|
|
9319
|
+
}
|
|
9320
|
+
async function resolveDocumentIdByExternalId(client, sel) {
|
|
9321
|
+
const err = validateSelector(sel, "document");
|
|
9322
|
+
if (err) return { error: err };
|
|
9323
|
+
if (sel.id) return { id: sel.id };
|
|
9324
|
+
const page = await client.documents.lookupDocumentsByBody({
|
|
9325
|
+
type: sel.type,
|
|
9326
|
+
field: "externalId",
|
|
9327
|
+
value: sel.externalId,
|
|
9328
|
+
limit: 1
|
|
9329
|
+
});
|
|
9330
|
+
const items = pageItems(page);
|
|
9331
|
+
if (items.length === 0) {
|
|
9332
|
+
return { error: `No document of type '${sel.type}' with externalId '${sel.externalId}'.` };
|
|
9333
|
+
}
|
|
9334
|
+
return { id: items[0].id };
|
|
9335
|
+
}
|
|
9336
|
+
|
|
9337
|
+
// src/tools/record_get.ts
|
|
9230
9338
|
var MAX_PAYLOAD_CHARS = 32e3;
|
|
9231
9339
|
var inputSchema3 = {
|
|
9232
|
-
id: import_zod3.z.string().
|
|
9340
|
+
id: import_zod3.z.string().optional().describe("The Vectros record id (e.g. from record_query or hybrid_search). Provide this OR externalId+type."),
|
|
9341
|
+
externalId: import_zod3.z.string().optional().describe(
|
|
9342
|
+
"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."
|
|
9343
|
+
),
|
|
9344
|
+
type: import_zod3.z.string().optional().describe("Record type \u2014 REQUIRED when selecting by `externalId` (externalId is unique within a type).")
|
|
9233
9345
|
};
|
|
9234
9346
|
var recordGet = ({ client, log }) => ({
|
|
9235
9347
|
name: "record_get",
|
|
9236
|
-
title: "Get a record by id",
|
|
9237
|
-
description: "Fetch a single structured record
|
|
9348
|
+
title: "Get a record by id or externalId",
|
|
9349
|
+
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.",
|
|
9238
9350
|
inputSchema: inputSchema3,
|
|
9239
9351
|
handler: async (args) => {
|
|
9240
9352
|
const id = args.id;
|
|
9353
|
+
const externalId = args.externalId;
|
|
9354
|
+
const type = args.type;
|
|
9241
9355
|
try {
|
|
9242
|
-
const
|
|
9356
|
+
const resolved = await resolveRecordIdByExternalId(client, { id, externalId, type });
|
|
9357
|
+
if ("error" in resolved) return toolError("record_get", new Error(resolved.error));
|
|
9358
|
+
const record = await client.records.getRecord({ id: resolved.id });
|
|
9243
9359
|
const out = { ...record };
|
|
9244
9360
|
if (record.payload !== void 0) {
|
|
9245
9361
|
const json = JSON.stringify(record.payload);
|
|
@@ -9251,12 +9367,12 @@ var recordGet = ({ client, log }) => ({
|
|
|
9251
9367
|
}
|
|
9252
9368
|
}
|
|
9253
9369
|
log.debug(
|
|
9254
|
-
{ tool: "record_get", id, truncated: out.payloadTruncated === true },
|
|
9370
|
+
{ tool: "record_get", id: resolved.id, externalId, truncated: out.payloadTruncated === true },
|
|
9255
9371
|
"record_get ok"
|
|
9256
9372
|
);
|
|
9257
9373
|
return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
|
|
9258
9374
|
} catch (err) {
|
|
9259
|
-
log.warn({ tool: "record_get", id, err: String(err) }, "record_get failed");
|
|
9375
|
+
log.warn({ tool: "record_get", id, externalId, type, err: String(err) }, "record_get failed");
|
|
9260
9376
|
return toolError("record_get", err);
|
|
9261
9377
|
}
|
|
9262
9378
|
}
|
|
@@ -9316,7 +9432,11 @@ var record_create_default = recordCreate;
|
|
|
9316
9432
|
// src/tools/record_update.ts
|
|
9317
9433
|
var import_zod5 = require("zod");
|
|
9318
9434
|
var inputSchema5 = {
|
|
9319
|
-
id: import_zod5.z.string().
|
|
9435
|
+
id: import_zod5.z.string().optional().describe("The Vectros record id to update. Provide this OR externalId+type."),
|
|
9436
|
+
externalId: import_zod5.z.string().optional().describe(
|
|
9437
|
+
"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."
|
|
9438
|
+
),
|
|
9439
|
+
type: import_zod5.z.string().optional().describe("Record type \u2014 REQUIRED when selecting by `externalId` (externalId is unique within a type)."),
|
|
9320
9440
|
fields: import_zod5.z.record(import_zod5.z.string(), import_zod5.z.unknown()).describe(
|
|
9321
9441
|
"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."
|
|
9322
9442
|
),
|
|
@@ -9327,23 +9447,27 @@ var inputSchema5 = {
|
|
|
9327
9447
|
};
|
|
9328
9448
|
var recordUpdate = ({ client, log }) => ({
|
|
9329
9449
|
name: "record_update",
|
|
9330
|
-
title: "Update a record",
|
|
9331
|
-
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.",
|
|
9450
|
+
title: "Update a record by id or externalId",
|
|
9451
|
+
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.",
|
|
9332
9452
|
inputSchema: inputSchema5,
|
|
9333
9453
|
handler: async (args) => {
|
|
9334
9454
|
const id = args.id;
|
|
9455
|
+
const externalId = args.externalId;
|
|
9456
|
+
const type = args.type;
|
|
9335
9457
|
const fields = args.fields;
|
|
9336
9458
|
const status = args.status;
|
|
9337
9459
|
const expectedVersion = args.expectedVersion;
|
|
9338
9460
|
try {
|
|
9461
|
+
const resolved = await resolveRecordIdByExternalId(client, { id, externalId, type });
|
|
9462
|
+
if ("error" in resolved) return toolError("record_update", new Error(resolved.error));
|
|
9339
9463
|
const body = { payload: fields };
|
|
9340
9464
|
if (status !== void 0) body.status = status;
|
|
9341
9465
|
if (expectedVersion !== void 0) body.expectedVersion = expectedVersion;
|
|
9342
|
-
const updated = await client.records.patchRecord({ id, body });
|
|
9343
|
-
log.debug({ tool: "record_update", id }, "record_update ok");
|
|
9466
|
+
const updated = await client.records.patchRecord({ id: resolved.id, body });
|
|
9467
|
+
log.debug({ tool: "record_update", id: resolved.id, externalId }, "record_update ok");
|
|
9344
9468
|
return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
|
|
9345
9469
|
} catch (err) {
|
|
9346
|
-
log.warn({ tool: "record_update", id, err: String(err) }, "record_update failed");
|
|
9470
|
+
log.warn({ tool: "record_update", id, externalId, type, err: String(err) }, "record_update failed");
|
|
9347
9471
|
return toolError("record_update", err);
|
|
9348
9472
|
}
|
|
9349
9473
|
}
|
|
@@ -9610,7 +9734,11 @@ var list_schemas_default = listSchemas;
|
|
|
9610
9734
|
var import_zod10 = require("zod");
|
|
9611
9735
|
var MAX_TEXT_CHARS = 32e3;
|
|
9612
9736
|
var inputSchema10 = {
|
|
9613
|
-
documentId: import_zod10.z.string().
|
|
9737
|
+
documentId: import_zod10.z.string().optional().describe("The document ID returned by document_ingest or hybrid_search. Provide this OR externalId+type."),
|
|
9738
|
+
externalId: import_zod10.z.string().optional().describe(
|
|
9739
|
+
"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."
|
|
9740
|
+
),
|
|
9741
|
+
type: import_zod10.z.string().optional().describe("Document type \u2014 REQUIRED when selecting by `externalId` (externalId is unique within a type)."),
|
|
9614
9742
|
includeText: import_zod10.z.boolean().optional().describe(
|
|
9615
9743
|
"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."
|
|
9616
9744
|
),
|
|
@@ -9630,13 +9758,18 @@ function isNotAvailable(err) {
|
|
|
9630
9758
|
var documentGet = ({ client, log }) => ({
|
|
9631
9759
|
name: "document_get",
|
|
9632
9760
|
title: "Get document metadata (and optionally text)",
|
|
9633
|
-
description: "Fetch metadata for a single document by
|
|
9761
|
+
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`.",
|
|
9634
9762
|
inputSchema: inputSchema10,
|
|
9635
9763
|
handler: async (args) => {
|
|
9636
|
-
const
|
|
9764
|
+
const documentId = args.documentId;
|
|
9765
|
+
const externalId = args.externalId;
|
|
9766
|
+
const type = args.type;
|
|
9637
9767
|
const includeText = args.includeText ?? false;
|
|
9638
9768
|
const includeDownloadUrl = args.includeDownloadUrl ?? false;
|
|
9639
9769
|
try {
|
|
9770
|
+
const resolved = await resolveDocumentIdByExternalId(client, { id: documentId, externalId, type });
|
|
9771
|
+
if ("error" in resolved) return toolError("document_get", new Error(resolved.error));
|
|
9772
|
+
const id = resolved.id;
|
|
9640
9773
|
const doc = await client.documents.getDocument({ id });
|
|
9641
9774
|
let extra = {};
|
|
9642
9775
|
if (includeText) {
|
|
@@ -9683,7 +9816,7 @@ var documentGet = ({ client, log }) => ({
|
|
|
9683
9816
|
]
|
|
9684
9817
|
};
|
|
9685
9818
|
} catch (err) {
|
|
9686
|
-
log.warn({ tool: "document_get",
|
|
9819
|
+
log.warn({ tool: "document_get", documentId, externalId, type, err: String(err) }, "document_get failed");
|
|
9687
9820
|
return toolError("document_get", err);
|
|
9688
9821
|
}
|
|
9689
9822
|
}
|
|
@@ -9798,13 +9931,23 @@ var document_query_default = documentQuery;
|
|
|
9798
9931
|
// src/tools/document_update.ts
|
|
9799
9932
|
var import_zod12 = require("zod");
|
|
9800
9933
|
var inputSchema12 = {
|
|
9801
|
-
documentId: import_zod12.z.string().
|
|
9934
|
+
documentId: import_zod12.z.string().optional().describe("The Vectros document id to update. Provide this OR externalId+type."),
|
|
9935
|
+
externalId: import_zod12.z.string().optional().describe(
|
|
9936
|
+
"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."
|
|
9937
|
+
),
|
|
9938
|
+
type: import_zod12.z.string().optional().describe("Document type \u2014 REQUIRED when selecting by `externalId` (externalId is unique within a type)."),
|
|
9802
9939
|
title: import_zod12.z.string().optional().describe("New title. Omit to keep the current title."),
|
|
9803
9940
|
fields: import_zod12.z.record(import_zod12.z.string(), import_zod12.z.unknown()).optional().describe(
|
|
9804
9941
|
"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."
|
|
9805
9942
|
),
|
|
9943
|
+
text: import_zod12.z.string().optional().describe(
|
|
9944
|
+
"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."
|
|
9945
|
+
),
|
|
9806
9946
|
folderId: import_zod12.z.string().optional().describe("Move the document into this folder (cannot be cleared once set)."),
|
|
9807
9947
|
storeText: import_zod12.z.boolean().optional().describe("Whether the raw text is stored + retrievable."),
|
|
9948
|
+
status: import_zod12.z.enum(["ACTIVE", "ARCHIVED"]).optional().describe(
|
|
9949
|
+
"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."
|
|
9950
|
+
),
|
|
9808
9951
|
userId: import_zod12.z.string().optional().describe("Reassign the owning user (Vectros UUID)."),
|
|
9809
9952
|
orgId: import_zod12.z.string().optional().describe("Reassign the owning org (Vectros UUID)."),
|
|
9810
9953
|
clientId: import_zod12.z.string().optional().describe("Reassign the associated client (Vectros UUID)."),
|
|
@@ -9815,33 +9958,41 @@ var inputSchema12 = {
|
|
|
9815
9958
|
var documentUpdate = ({ client, log }) => ({
|
|
9816
9959
|
name: "document_update",
|
|
9817
9960
|
title: "Update a document",
|
|
9818
|
-
description:
|
|
9961
|
+
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.',
|
|
9819
9962
|
inputSchema: inputSchema12,
|
|
9820
9963
|
handler: async (args) => {
|
|
9821
9964
|
const documentId = args.documentId;
|
|
9965
|
+
const externalId = args.externalId;
|
|
9966
|
+
const type = args.type;
|
|
9822
9967
|
const title = args.title;
|
|
9823
9968
|
const fields = args.fields;
|
|
9969
|
+
const text = args.text;
|
|
9824
9970
|
const folderId = args.folderId;
|
|
9825
9971
|
const storeText = args.storeText;
|
|
9972
|
+
const status = args.status;
|
|
9826
9973
|
const userId = args.userId;
|
|
9827
9974
|
const orgId = args.orgId;
|
|
9828
9975
|
const clientId = args.clientId;
|
|
9829
9976
|
const expectedVersion = args.expectedVersion;
|
|
9830
9977
|
try {
|
|
9978
|
+
const resolved = await resolveDocumentIdByExternalId(client, { id: documentId, externalId, type });
|
|
9979
|
+
if ("error" in resolved) return toolError("document_update", new Error(resolved.error));
|
|
9831
9980
|
const body = {};
|
|
9832
9981
|
if (title !== void 0) body.title = title;
|
|
9833
9982
|
if (fields !== void 0) body.payload = fields;
|
|
9983
|
+
if (text !== void 0) body.text = text;
|
|
9834
9984
|
if (folderId !== void 0) body.folderId = folderId;
|
|
9835
9985
|
if (storeText !== void 0) body.storeText = storeText;
|
|
9986
|
+
if (status !== void 0) body.status = status;
|
|
9836
9987
|
if (userId !== void 0) body.userId = userId;
|
|
9837
9988
|
if (orgId !== void 0) body.orgId = orgId;
|
|
9838
9989
|
if (clientId !== void 0) body.clientId = clientId;
|
|
9839
9990
|
if (expectedVersion !== void 0) body.expectedVersion = expectedVersion;
|
|
9840
9991
|
const updated = await client.documents.patchDocument({
|
|
9841
|
-
id:
|
|
9992
|
+
id: resolved.id,
|
|
9842
9993
|
body
|
|
9843
9994
|
});
|
|
9844
|
-
log.debug({ tool: "document_update", documentId }, "document_update ok");
|
|
9995
|
+
log.debug({ tool: "document_update", documentId: resolved.id, externalId }, "document_update ok");
|
|
9845
9996
|
return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
|
|
9846
9997
|
} catch (err) {
|
|
9847
9998
|
log.warn({ tool: "document_update", documentId, err: String(err) }, "document_update failed");
|
|
@@ -9859,7 +10010,7 @@ var inputSchema13 = {
|
|
|
9859
10010
|
var documentDelete = ({ client, log }) => ({
|
|
9860
10011
|
name: "document_delete",
|
|
9861
10012
|
title: "Delete a document",
|
|
9862
|
-
description:
|
|
10013
|
+
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).',
|
|
9863
10014
|
inputSchema: inputSchema13,
|
|
9864
10015
|
handler: async (args) => {
|
|
9865
10016
|
const documentId = args.documentId;
|
|
@@ -10139,6 +10290,9 @@ var inputSchema19 = {
|
|
|
10139
10290
|
externalId: import_zod18.z.string().optional().describe(
|
|
10140
10291
|
"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."
|
|
10141
10292
|
),
|
|
10293
|
+
upsert: import_zod18.z.boolean().optional().describe(
|
|
10294
|
+
"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)."
|
|
10295
|
+
),
|
|
10142
10296
|
schemaId: import_zod18.z.string().optional().describe(
|
|
10143
10297
|
"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."
|
|
10144
10298
|
),
|
|
@@ -10164,13 +10318,14 @@ var inputSchema19 = {
|
|
|
10164
10318
|
var documentIngest = ({ client, log, transport, ingestRoot }) => ({
|
|
10165
10319
|
name: "document_ingest",
|
|
10166
10320
|
title: "Ingest a new document (text body or file upload)",
|
|
10167
|
-
description: "Create a new document in
|
|
10321
|
+
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).",
|
|
10168
10322
|
inputSchema: inputSchema19,
|
|
10169
10323
|
handler: async (args) => {
|
|
10170
10324
|
const title = args.title;
|
|
10171
10325
|
const text = args.text;
|
|
10172
10326
|
const filePath = args.filePath;
|
|
10173
10327
|
const schemaId = args.schemaId;
|
|
10328
|
+
const upsert = args.upsert;
|
|
10174
10329
|
const indexMode = args.indexMode ?? (schemaId ? void 0 : "HYBRID");
|
|
10175
10330
|
if (!text && !filePath) {
|
|
10176
10331
|
return toolError(
|
|
@@ -10205,6 +10360,7 @@ var documentIngest = ({ client, log, transport, ingestRoot }) => ({
|
|
|
10205
10360
|
if (text) {
|
|
10206
10361
|
const storeText = args.storeText ?? true;
|
|
10207
10362
|
const result = await client.documents.ingestDocument({
|
|
10363
|
+
upsert,
|
|
10208
10364
|
body: {
|
|
10209
10365
|
title,
|
|
10210
10366
|
text,
|
|
@@ -10241,6 +10397,7 @@ var documentIngest = ({ client, log, transport, ingestRoot }) => ({
|
|
|
10241
10397
|
const fileName = (0, import_node_path.basename)(safePath);
|
|
10242
10398
|
const fileType = args.fileType ?? inferMimeType(safePath);
|
|
10243
10399
|
const upload = await client.documents.uploadDocument({
|
|
10400
|
+
upsert,
|
|
10244
10401
|
fileName,
|
|
10245
10402
|
fileType,
|
|
10246
10403
|
indexMode,
|
|
@@ -10279,8 +10436,8 @@ var documentIngest = ({ client, log, transport, ingestRoot }) => ({
|
|
|
10279
10436
|
text: JSON.stringify(
|
|
10280
10437
|
{
|
|
10281
10438
|
...upload,
|
|
10282
|
-
|
|
10283
|
-
_note: "File uploaded; indexing is asynchronous. Poll document_get(id) until
|
|
10439
|
+
indexStatus: "PENDING_INDEX",
|
|
10440
|
+
_note: "File uploaded; indexing is asynchronous. Poll document_get(id) until indexStatus is INDEXED."
|
|
10284
10441
|
},
|
|
10285
10442
|
null,
|
|
10286
10443
|
2
|
|
@@ -10577,6 +10734,14 @@ function zodTypeToJsonSchema(schema) {
|
|
|
10577
10734
|
|
|
10578
10735
|
// src/server.ts
|
|
10579
10736
|
var DEFAULT_ENVIRONMENT = "https://api.vectros.ai";
|
|
10737
|
+
function formatArgError(tool, error) {
|
|
10738
|
+
const unknownKeys = error.issues.filter((i) => i.code === "unrecognized_keys").flatMap((i) => i.keys);
|
|
10739
|
+
if (unknownKeys.length > 0) {
|
|
10740
|
+
const valid = Object.keys(tool.inputSchema).join(", ");
|
|
10741
|
+
return `Unknown argument(s): ${unknownKeys.join(", ")}. Valid arguments for ${tool.name}: ${valid}. Pass only these \u2014 an unrecognized argument is rejected, not ignored.`;
|
|
10742
|
+
}
|
|
10743
|
+
return `Invalid arguments: ${error.message}`;
|
|
10744
|
+
}
|
|
10580
10745
|
var VectrosMCPServer = class {
|
|
10581
10746
|
server;
|
|
10582
10747
|
log;
|
|
@@ -10666,7 +10831,11 @@ var VectrosMCPServer = class {
|
|
|
10666
10831
|
name: t.name,
|
|
10667
10832
|
title: t.title,
|
|
10668
10833
|
description: t.description,
|
|
10669
|
-
|
|
10834
|
+
// additionalProperties:false advertises strict argument validation to the
|
|
10835
|
+
// MCP client (a well-behaved client rejects an unknown key before the call).
|
|
10836
|
+
// The server enforces it too (z.object(...).strict() below) so a raw JSON-RPC
|
|
10837
|
+
// call that skips client validation is still rejected — belt and suspenders.
|
|
10838
|
+
inputSchema: { ...zodShapeToJsonSchema(t.inputSchema), additionalProperties: false }
|
|
10670
10839
|
}))
|
|
10671
10840
|
})
|
|
10672
10841
|
);
|
|
@@ -10676,9 +10845,9 @@ var VectrosMCPServer = class {
|
|
|
10676
10845
|
if (!tool) {
|
|
10677
10846
|
result = toolError(req.params.name ?? "unknown", new Error("No such tool."));
|
|
10678
10847
|
} else {
|
|
10679
|
-
const parsed = import_zod22.z.object(tool.inputSchema).safeParse(req.params.arguments ?? {});
|
|
10848
|
+
const parsed = import_zod22.z.object(tool.inputSchema).strict().safeParse(req.params.arguments ?? {});
|
|
10680
10849
|
if (!parsed.success) {
|
|
10681
|
-
result = toolError(tool.name, new Error(
|
|
10850
|
+
result = toolError(tool.name, new Error(formatArgError(tool, parsed.error)));
|
|
10682
10851
|
} else {
|
|
10683
10852
|
try {
|
|
10684
10853
|
result = await tool.handler(
|