@typesensekit/mcp
Advanced tools
| #!/usr/bin/env node | ||
| // src/server.ts | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| // ../core/src/client.ts | ||
| import Typesense from "typesense"; | ||
| // ../core/src/config.ts | ||
| import { z } from "zod"; | ||
| var nodeConfigSchema = z.object({ | ||
| host: z.string().min(1), | ||
| port: z.number().int().positive().optional(), | ||
| protocol: z.enum(["http", "https"]).optional(), | ||
| path: z.string().optional() | ||
| }); | ||
| var serverConfigSchema = z.object({ | ||
| url: z.string().url(), | ||
| apiKey: z.string().min(1), | ||
| connectionTimeoutSeconds: z.number().positive().optional(), | ||
| nearestNode: nodeConfigSchema.optional(), | ||
| numRetries: z.number().int().nonnegative().optional(), | ||
| retryIntervalSeconds: z.number().positive().optional(), | ||
| healthcheckIntervalSeconds: z.number().positive().optional() | ||
| }); | ||
| // ../core/src/client.ts | ||
| function nodeFromUrl(url) { | ||
| const parsed = new URL(url); | ||
| const node = { | ||
| host: parsed.hostname, | ||
| port: parsed.port ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80, | ||
| protocol: parsed.protocol.replace(":", "") | ||
| }; | ||
| if (parsed.pathname !== "/") node.path = parsed.pathname; | ||
| return node; | ||
| } | ||
| function normalizeNearestNode(node) { | ||
| if (!node) return void 0; | ||
| return { | ||
| host: node.host, | ||
| port: node.port ?? (node.protocol === "http" ? 80 : 443), | ||
| protocol: node.protocol ?? "https", | ||
| path: node.path | ||
| }; | ||
| } | ||
| function createClient(config) { | ||
| const parsed = serverConfigSchema.parse(config); | ||
| const clientConfig = { | ||
| nodes: [nodeFromUrl(parsed.url)], | ||
| apiKey: parsed.apiKey, | ||
| connectionTimeoutSeconds: parsed.connectionTimeoutSeconds, | ||
| nearestNode: normalizeNearestNode(parsed.nearestNode), | ||
| numRetries: parsed.numRetries, | ||
| retryIntervalSeconds: parsed.retryIntervalSeconds, | ||
| healthcheckIntervalSeconds: parsed.healthcheckIntervalSeconds | ||
| }; | ||
| return new Typesense.Client(clientConfig); | ||
| } | ||
| // ../core/src/redaction.ts | ||
| var REDACTED = "[REDACTED]"; | ||
| var CIRCULAR = "[Circular]"; | ||
| var SECRET_KEYS = /* @__PURE__ */ new Set([ | ||
| "api_key", | ||
| "apikey", | ||
| "authorization", | ||
| "cookie", | ||
| "secret", | ||
| "setcookie", | ||
| "token", | ||
| "xtypesenseapikey" | ||
| ]); | ||
| function shouldRedactKey(key) { | ||
| const normalized = key.toLowerCase().replace(/[-_\s]/g, ""); | ||
| return SECRET_KEYS.has(normalized) || normalized.endsWith("apikey") || normalized.endsWith("token") || normalized.endsWith("secret"); | ||
| } | ||
| function isTypesenseApiKeyShape(value) { | ||
| return Array.isArray(value.actions) && Array.isArray(value.collections); | ||
| } | ||
| function redactText(value) { | ||
| return value.replace( | ||
| /(["']?authorization["']?\s*[:=]\s*)(["']?)(?:Bearer|Basic)?\s*[-A-Za-z0-9._~+/=]+(["']?)/gi, | ||
| (_match, prefix, openQuote) => `${prefix}${openQuote}${REDACTED}${openQuote}` | ||
| ).replace( | ||
| /(["']?(?:x-typesense-api-key|api[_-]?key|apikey|token|secret|cookie|set-cookie)["']?\s*[:=]\s*)(["']?)([^"',\s}\]]+)(["']?)/gi, | ||
| (_match, prefix, openQuote, _secret) => `${prefix}${openQuote}${REDACTED}${openQuote}` | ||
| ).replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, `$1 ${REDACTED}`); | ||
| } | ||
| function redactError(error, seen) { | ||
| const output = {}; | ||
| for (const key of Object.getOwnPropertyNames(error)) { | ||
| output[key] = redactValue( | ||
| error[key], | ||
| seen | ||
| ); | ||
| } | ||
| if (!("name" in output)) output.name = error.name; | ||
| if (!("message" in output)) output.message = redactText(error.message); | ||
| return output; | ||
| } | ||
| function redactValue(value, seen) { | ||
| if (typeof value === "string") return redactText(value); | ||
| if (Array.isArray(value)) { | ||
| return value.map((item) => redactValue(item, seen)); | ||
| } | ||
| if (typeof value !== "object" || value === null) { | ||
| return value; | ||
| } | ||
| if (seen.has(value)) return CIRCULAR; | ||
| seen.add(value); | ||
| if (value instanceof Error) { | ||
| return redactError(value, seen); | ||
| } | ||
| const record = value; | ||
| return Object.fromEntries( | ||
| Object.entries(record).map(([key, child]) => [ | ||
| key, | ||
| shouldRedactKey(key) || key === "value" && isTypesenseApiKeyShape(record) ? REDACTED : redactValue(child, seen) | ||
| ]) | ||
| ); | ||
| } | ||
| function redactSecrets(value) { | ||
| return redactValue(value, /* @__PURE__ */ new WeakSet()); | ||
| } | ||
| // ../core/src/errors.ts | ||
| var NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([ | ||
| "EAI_AGAIN", | ||
| "ECONNABORTED", | ||
| "ECONNREFUSED", | ||
| "ECONNRESET", | ||
| "ENOTFOUND", | ||
| "ETIMEDOUT" | ||
| ]); | ||
| function readErrorLike(error) { | ||
| return typeof error === "object" && error !== null ? error : {}; | ||
| } | ||
| function findNetworkErrorCode(error) { | ||
| const errorLike = readErrorLike(error); | ||
| if (typeof errorLike.code === "string" && NETWORK_ERROR_CODES.has(errorLike.code.toUpperCase())) { | ||
| return errorLike.code.toUpperCase(); | ||
| } | ||
| if (typeof errorLike.message === "string") { | ||
| const match = errorLike.message.match( | ||
| /\b(EAI_AGAIN|ECONNABORTED|ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT)\b/i | ||
| ); | ||
| if (match?.[1]) return match[1].toUpperCase(); | ||
| } | ||
| return errorLike.cause === void 0 ? void 0 : findNetworkErrorCode(errorLike.cause); | ||
| } | ||
| function endpointLabel(error) { | ||
| const errorLike = readErrorLike(error); | ||
| const host = typeof errorLike.hostname === "string" ? errorLike.hostname : typeof errorLike.host === "string" ? errorLike.host : typeof errorLike.address === "string" ? errorLike.address : void 0; | ||
| const port = typeof errorLike.port === "number" || typeof errorLike.port === "string" ? String(errorLike.port) : void 0; | ||
| if (host && port) return `${host}:${port}`; | ||
| if (host) return host; | ||
| return errorLike.cause === void 0 ? void 0 : endpointLabel(errorLike.cause); | ||
| } | ||
| function conciseNetworkErrorMessage(error) { | ||
| const code = findNetworkErrorCode(error); | ||
| if (!code) return void 0; | ||
| const endpoint = endpointLabel(error); | ||
| return `Request failed: ${code}${endpoint ? ` ${endpoint}` : ""}`; | ||
| } | ||
| function normalizeTypesenseError(error) { | ||
| if (error instanceof Error) { | ||
| const errorLike = error; | ||
| const status = errorLike.httpStatus ?? errorLike.status; | ||
| return { | ||
| code: typeof status === "number" ? String(status) : error.name || "TypesenseError", | ||
| message: redactText(error.message), | ||
| details: redactSecrets(error) | ||
| }; | ||
| } | ||
| if (typeof error === "object" && error !== null) { | ||
| const errorLike = error; | ||
| return { | ||
| code: typeof errorLike.status === "number" ? String(errorLike.status) : "TypesenseError", | ||
| message: typeof errorLike.message === "string" ? redactText(errorLike.message) : "Unknown Typesense error", | ||
| details: redactSecrets(error) | ||
| }; | ||
| } | ||
| return { code: "TypesenseError", message: redactText(String(error)) }; | ||
| } | ||
| function formatTypesenseErrorMessage(error, options = {}) { | ||
| const normalized = normalizeTypesenseError(error); | ||
| const message = conciseNetworkErrorMessage(error) ?? normalized.message; | ||
| if (!options.debug) return message; | ||
| return [ | ||
| message, | ||
| "", | ||
| "Debug details:", | ||
| JSON.stringify(normalized.details ?? normalized, null, 2) | ||
| ].join("\n"); | ||
| } | ||
| // ../core/src/operations/aliases.ts | ||
| import { z as z2 } from "zod"; | ||
| // ../core/src/operations/http.ts | ||
| function api(client) { | ||
| return client.apiCall; | ||
| } | ||
| function enc(value) { | ||
| return encodeURIComponent(value); | ||
| } | ||
| function collectionPath(collection) { | ||
| return `/collections/${enc(collection)}`; | ||
| } | ||
| // ../core/src/operations/aliases.ts | ||
| var aliasesOperations = [ | ||
| { | ||
| name: "aliases.list", | ||
| summary: "List aliases", | ||
| category: "aliases", | ||
| input: z2.object({}), | ||
| execute: async (client) => api(client).get("/aliases") | ||
| }, | ||
| { | ||
| name: "aliases.create", | ||
| summary: "Create or upsert an alias", | ||
| category: "aliases", | ||
| input: z2.object({ | ||
| name: z2.string(), | ||
| value: z2.object({ collection_name: z2.string() }) | ||
| }), | ||
| execute: async (client, input) => api(client).put(`/aliases/${enc(input.name)}`, input.value) | ||
| }, | ||
| { | ||
| name: "aliases.retrieve", | ||
| summary: "Retrieve an alias", | ||
| category: "aliases", | ||
| input: z2.object({ name: z2.string() }), | ||
| execute: async (client, input) => api(client).get(`/aliases/${enc(input.name)}`) | ||
| }, | ||
| { | ||
| name: "aliases.delete", | ||
| summary: "Delete an alias", | ||
| category: "aliases", | ||
| input: z2.object({ name: z2.string() }), | ||
| execute: async (client, input) => api(client).delete(`/aliases/${enc(input.name)}`) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/analytics.ts | ||
| import { z as z3 } from "zod"; | ||
| var analyticsOperations = [ | ||
| { | ||
| name: "analytics.rules.list", | ||
| summary: "List analytics rules", | ||
| category: "analytics", | ||
| input: z3.object({ ruleTag: z3.string().optional() }), | ||
| execute: async (client, input) => api(client).get( | ||
| "/analytics/rules", | ||
| input.ruleTag ? { rule_tag: input.ruleTag } : void 0 | ||
| ) | ||
| }, | ||
| { | ||
| name: "analytics.rules.create", | ||
| summary: "Create one or more analytics rules", | ||
| category: "analytics", | ||
| input: z3.object({ | ||
| value: z3.union([ | ||
| z3.record(z3.unknown()), | ||
| z3.array(z3.record(z3.unknown())).min(1) | ||
| ]) | ||
| }), | ||
| execute: async (client, input) => api(client).post("/analytics/rules", input.value) | ||
| }, | ||
| { | ||
| name: "analytics.rules.upsert", | ||
| summary: "Create or update an analytics rule", | ||
| category: "analytics", | ||
| input: z3.object({ name: z3.string(), value: z3.record(z3.unknown()) }), | ||
| execute: async (client, input) => api(client).put(`/analytics/rules/${enc(input.name)}`, input.value) | ||
| }, | ||
| { | ||
| name: "analytics.rules.delete", | ||
| summary: "Delete an analytics rule", | ||
| category: "analytics", | ||
| input: z3.object({ name: z3.string() }), | ||
| execute: async (client, input) => api(client).delete(`/analytics/rules/${enc(input.name)}`) | ||
| }, | ||
| { | ||
| name: "analytics.rules.retrieve", | ||
| summary: "Retrieve an analytics rule", | ||
| category: "analytics", | ||
| input: z3.object({ name: z3.string() }), | ||
| execute: async (client, input) => api(client).get(`/analytics/rules/${enc(input.name)}`) | ||
| }, | ||
| { | ||
| name: "analytics.events.create", | ||
| summary: "Create an analytics event", | ||
| category: "analytics", | ||
| input: z3.object({ | ||
| type: z3.string(), | ||
| name: z3.string(), | ||
| data: z3.record(z3.unknown()) | ||
| }), | ||
| execute: async (client, input) => api(client).post("/analytics/events", input) | ||
| }, | ||
| { | ||
| name: "analytics.events.list", | ||
| summary: "Retrieve recent analytics events for a user and rule", | ||
| category: "analytics", | ||
| input: z3.object({ | ||
| userId: z3.string().min(1), | ||
| name: z3.string().min(1), | ||
| limit: z3.number().int().positive().max(1e3) | ||
| }), | ||
| execute: async (client, input) => api(client).get("/analytics/events", { | ||
| user_id: input.userId, | ||
| name: input.name, | ||
| n: input.limit | ||
| }) | ||
| }, | ||
| { | ||
| name: "analytics.flush", | ||
| summary: "Flush in-memory analytics data to persistent storage", | ||
| category: "analytics", | ||
| input: z3.object({}), | ||
| execute: async (client) => api(client).post("/analytics/flush") | ||
| }, | ||
| { | ||
| name: "analytics.status", | ||
| summary: "Retrieve analytics subsystem status", | ||
| category: "analytics", | ||
| input: z3.object({}), | ||
| execute: async (client) => api(client).get("/analytics/status") | ||
| } | ||
| ]; | ||
| // ../core/src/operations/api.ts | ||
| import { z as z4 } from "zod"; | ||
| var methodSchema = z4.enum(["get", "post", "put", "patch", "delete"]); | ||
| var normalizedMethodSchema = z4.preprocess( | ||
| (value) => typeof value === "string" ? value.toLowerCase() : value, | ||
| methodSchema | ||
| ); | ||
| var apiOperations = [ | ||
| { | ||
| name: "api.call", | ||
| summary: "Call any Typesense API endpoint not yet covered by a first-class operation", | ||
| category: "api", | ||
| input: z4.object({ | ||
| method: normalizedMethodSchema, | ||
| path: z4.string().startsWith("/"), | ||
| params: z4.record(z4.unknown()).optional(), | ||
| body: z4.unknown().optional() | ||
| }), | ||
| execute: async (client, input) => { | ||
| const request = api(client); | ||
| if (input.method === "get") return request.get(input.path, input.params); | ||
| if (input.method === "delete") | ||
| return request.delete(input.path, input.params); | ||
| if (input.method === "post") | ||
| return request.post(input.path, input.body, input.params); | ||
| if (input.method === "put") | ||
| return request.put(input.path, input.body, input.params); | ||
| return request.patch(input.path, input.body, input.params); | ||
| } | ||
| } | ||
| ]; | ||
| // ../core/src/operations/collections.ts | ||
| import { z as z5 } from "zod"; | ||
| var DEFAULT_WAIT_TIMEOUT_MS = 3e4; | ||
| var DEFAULT_WAIT_INTERVAL_MS = 1e3; | ||
| var baseFieldSchema = z5.object({ | ||
| name: z5.string(), | ||
| facet: z5.boolean().optional(), | ||
| index: z5.boolean().optional(), | ||
| optional: z5.boolean().optional(), | ||
| sort: z5.boolean().optional(), | ||
| locale: z5.string().optional(), | ||
| infix: z5.boolean().optional(), | ||
| stem: z5.boolean().optional() | ||
| }).passthrough(); | ||
| var createFieldSchema = baseFieldSchema.extend({ | ||
| type: z5.string() | ||
| }); | ||
| var patchFieldSchema = baseFieldSchema.extend({ | ||
| type: z5.string().optional(), | ||
| drop: z5.boolean().optional() | ||
| }); | ||
| var fieldLifecycleInputSchema = z5.object({ | ||
| collection: z5.string(), | ||
| field: z5.string().optional(), | ||
| yes: z5.boolean().optional(), | ||
| numDim: z5.coerce.number().int().positive().optional(), | ||
| vecDist: z5.string().optional(), | ||
| hnswM: z5.coerce.number().int().positive().optional(), | ||
| hnswEfConstruction: z5.coerce.number().int().positive().optional(), | ||
| embedFrom: z5.union([z5.string(), z5.array(z5.string())]).optional(), | ||
| embedModel: z5.string().optional(), | ||
| embedApiKey: z5.string().optional(), | ||
| timeoutMs: z5.coerce.number().int().nonnegative().default(DEFAULT_WAIT_TIMEOUT_MS), | ||
| intervalMs: z5.coerce.number().int().nonnegative().default(DEFAULT_WAIT_INTERVAL_MS) | ||
| }).passthrough(); | ||
| var waitInputSchema = z5.object({ | ||
| collection: z5.string(), | ||
| fieldPresent: z5.string().optional(), | ||
| fieldMissing: z5.string().optional(), | ||
| fieldEmbedFrom: z5.string().optional(), | ||
| timeoutMs: z5.coerce.number().int().nonnegative().default(DEFAULT_WAIT_TIMEOUT_MS), | ||
| intervalMs: z5.coerce.number().int().nonnegative().default(DEFAULT_WAIT_INTERVAL_MS) | ||
| }); | ||
| var FIELD_LIFECYCLE_KEYS = /* @__PURE__ */ new Set([ | ||
| "collection", | ||
| "field", | ||
| "yes", | ||
| "numDim", | ||
| "vecDist", | ||
| "hnswM", | ||
| "hnswEfConstruction", | ||
| "embedFrom", | ||
| "embedModel", | ||
| "embedApiKey", | ||
| "timeoutMs", | ||
| "intervalMs" | ||
| ]); | ||
| function sleep(ms) { | ||
| return new Promise((resolve) => setTimeout(resolve, ms)); | ||
| } | ||
| function isObject(value) { | ||
| return typeof value === "object" && value !== null && !Array.isArray(value); | ||
| } | ||
| function requestedFieldName(input) { | ||
| const name = input.field ?? input.name; | ||
| if (typeof name !== "string" || !name) { | ||
| throw new Error("A field name is required. Pass --field or include name."); | ||
| } | ||
| return name; | ||
| } | ||
| function requiresConfirmation(collection) { | ||
| return /^production(?:__|-|$)/i.test(collection) || /^prod(?:__|-|$)/i.test(collection); | ||
| } | ||
| function assertDestructiveConfirmed(input) { | ||
| if (requiresConfirmation(input.collection) && !input.yes) { | ||
| throw new Error( | ||
| `Refusing to modify production collection ${input.collection} without --yes.` | ||
| ); | ||
| } | ||
| } | ||
| function findField(collection, fieldName) { | ||
| return collection.fields?.find((field) => { | ||
| if (!isObject(field)) return false; | ||
| return field.name === fieldName; | ||
| }); | ||
| } | ||
| async function retrieveCollection(client, collection) { | ||
| return api(client).get(collectionPath(collection)); | ||
| } | ||
| function buildFieldDefinition(input) { | ||
| const field = Object.fromEntries( | ||
| Object.entries(input).filter( | ||
| ([key, value]) => value !== void 0 && !FIELD_LIFECYCLE_KEYS.has(key) | ||
| ) | ||
| ); | ||
| field.name = requestedFieldName(input); | ||
| if (input.numDim !== void 0) field.num_dim = input.numDim; | ||
| if (input.vecDist !== void 0) field.vec_dist = input.vecDist; | ||
| if (input.hnswM !== void 0 || input.hnswEfConstruction !== void 0) { | ||
| const hnswParams = isObject(field.hnsw_params) ? field.hnsw_params : {}; | ||
| if (input.hnswM !== void 0) hnswParams.M = input.hnswM; | ||
| if (input.hnswEfConstruction !== void 0) { | ||
| hnswParams.ef_construction = input.hnswEfConstruction; | ||
| } | ||
| field.hnsw_params = hnswParams; | ||
| } | ||
| if (input.embedFrom !== void 0 || input.embedModel !== void 0 || input.embedApiKey !== void 0) { | ||
| const embed = isObject(field.embed) ? field.embed : {}; | ||
| const modelConfig = isObject(embed.model_config) ? embed.model_config : {}; | ||
| if (input.embedFrom !== void 0) { | ||
| embed.from = Array.isArray(input.embedFrom) ? input.embedFrom : [input.embedFrom]; | ||
| } | ||
| if (input.embedModel !== void 0) | ||
| modelConfig.model_name = input.embedModel; | ||
| if (input.embedApiKey !== void 0) | ||
| modelConfig.api_key = input.embedApiKey; | ||
| embed.model_config = modelConfig; | ||
| field.embed = embed; | ||
| } | ||
| if (typeof field.type !== "string" || !field.type) { | ||
| throw new Error("A field type is required. Pass --type or include type."); | ||
| } | ||
| return field; | ||
| } | ||
| function parseEmbedFromCondition(condition) { | ||
| const separator = condition.indexOf(":"); | ||
| if (separator === -1) { | ||
| throw new Error("--field-embed-from must use FIELD:SOURCE format."); | ||
| } | ||
| return { | ||
| fieldName: condition.slice(0, separator), | ||
| source: condition.slice(separator + 1) | ||
| }; | ||
| } | ||
| function isTransientCollectionUpdateError(error) { | ||
| const message = error instanceof Error ? error.message : String(error); | ||
| return /another collection update operation is in progress/i.test(message) || /timeout of \d+ms exceeded/i.test(message) || /econnaborted/i.test(message); | ||
| } | ||
| function evaluateWaitCondition(collection, input) { | ||
| const conditions = [ | ||
| input.fieldPresent, | ||
| input.fieldMissing, | ||
| input.fieldEmbedFrom | ||
| ].filter(Boolean); | ||
| if (conditions.length !== 1) { | ||
| throw new Error( | ||
| "Pass exactly one wait condition: --field-present, --field-missing, or --field-embed-from." | ||
| ); | ||
| } | ||
| if (input.fieldPresent) { | ||
| return { | ||
| ok: Boolean(findField(collection, input.fieldPresent)), | ||
| condition: "field-present", | ||
| field: input.fieldPresent | ||
| }; | ||
| } | ||
| if (input.fieldMissing) { | ||
| return { | ||
| ok: !findField(collection, input.fieldMissing), | ||
| condition: "field-missing", | ||
| field: input.fieldMissing | ||
| }; | ||
| } | ||
| const { fieldName, source } = parseEmbedFromCondition( | ||
| input.fieldEmbedFrom ?? "" | ||
| ); | ||
| const field = findField(collection, fieldName); | ||
| const from = isObject(field) && isObject(field.embed) && Array.isArray(field.embed.from) ? field.embed.from : []; | ||
| return { | ||
| ok: from.includes(source), | ||
| condition: "field-embed-from", | ||
| field: fieldName, | ||
| source | ||
| }; | ||
| } | ||
| async function waitForCollectionCondition(client, input) { | ||
| const startedAt = Date.now(); | ||
| let lastError; | ||
| let attempts = 0; | ||
| while (Date.now() - startedAt <= input.timeoutMs) { | ||
| attempts += 1; | ||
| try { | ||
| const collection = await retrieveCollection(client, input.collection); | ||
| const result = evaluateWaitCondition(collection, input); | ||
| if (result.ok) { | ||
| return { | ||
| collection: input.collection, | ||
| attempts, | ||
| ...result | ||
| }; | ||
| } | ||
| } catch (error) { | ||
| if (!isTransientCollectionUpdateError(error)) throw error; | ||
| lastError = error; | ||
| } | ||
| await sleep(input.intervalMs); | ||
| } | ||
| const detail = lastError instanceof Error ? ` Last error: ${lastError.message}` : ""; | ||
| throw new Error( | ||
| `Timed out waiting for collection ${input.collection} schema condition.${detail}` | ||
| ); | ||
| } | ||
| async function dropField(client, input) { | ||
| assertDestructiveConfirmed(input); | ||
| const fieldName = requestedFieldName(input); | ||
| const collection = await retrieveCollection(client, input.collection); | ||
| const existingField = findField(collection, fieldName); | ||
| if (!existingField) { | ||
| return { | ||
| ok: true, | ||
| collection: input.collection, | ||
| field: fieldName, | ||
| alreadyMissing: true | ||
| }; | ||
| } | ||
| const result = await api(client).patch(collectionPath(input.collection), { | ||
| fields: [{ name: fieldName, drop: true }] | ||
| }); | ||
| return { | ||
| ok: true, | ||
| collection: input.collection, | ||
| field: fieldName, | ||
| before: existingField, | ||
| result | ||
| }; | ||
| } | ||
| async function addField(client, input) { | ||
| const field = buildFieldDefinition(input); | ||
| const result = await api(client).patch(collectionPath(input.collection), { | ||
| fields: [field] | ||
| }); | ||
| return { | ||
| ok: true, | ||
| collection: input.collection, | ||
| field: field.name, | ||
| added: field, | ||
| result | ||
| }; | ||
| } | ||
| var collectionOperations = [ | ||
| { | ||
| name: "collections.create", | ||
| summary: "Create a collection", | ||
| category: "collections", | ||
| input: z5.object({ | ||
| name: z5.string(), | ||
| fields: z5.array(createFieldSchema), | ||
| default_sorting_field: z5.string().optional(), | ||
| token_separators: z5.array(z5.string()).optional(), | ||
| symbols_to_index: z5.array(z5.string()).optional(), | ||
| enable_nested_fields: z5.boolean().optional() | ||
| }), | ||
| execute: async (client, input) => api(client).post("/collections", input) | ||
| }, | ||
| { | ||
| name: "collections.list", | ||
| summary: "List collections", | ||
| category: "collections", | ||
| input: z5.object({}), | ||
| execute: async (client) => api(client).get("/collections") | ||
| }, | ||
| { | ||
| name: "collections.retrieve", | ||
| summary: "Retrieve a collection", | ||
| category: "collections", | ||
| input: z5.object({ collection: z5.string() }), | ||
| execute: async (client, input) => api(client).get(collectionPath(input.collection)) | ||
| }, | ||
| { | ||
| name: "collections.update", | ||
| summary: "Update a collection schema", | ||
| category: "collections", | ||
| input: z5.object({ | ||
| collection: z5.string(), | ||
| fields: z5.array(patchFieldSchema).optional() | ||
| }), | ||
| execute: async (client, input) => api(client).patch(collectionPath(input.collection), { | ||
| fields: input.fields | ||
| }) | ||
| }, | ||
| { | ||
| name: "collections.wait", | ||
| summary: "Wait for a collection schema condition", | ||
| category: "collections", | ||
| input: waitInputSchema, | ||
| execute: waitForCollectionCondition | ||
| }, | ||
| { | ||
| name: "collections.fields.add", | ||
| summary: "Add a field to a collection", | ||
| category: "collections", | ||
| input: fieldLifecycleInputSchema, | ||
| execute: addField | ||
| }, | ||
| { | ||
| name: "collections.fields.drop", | ||
| summary: "Drop a field from a collection", | ||
| category: "collections", | ||
| input: fieldLifecycleInputSchema, | ||
| execute: dropField | ||
| }, | ||
| { | ||
| name: "collections.fields.replace", | ||
| summary: "Safely replace a collection field", | ||
| category: "collections", | ||
| input: fieldLifecycleInputSchema, | ||
| execute: async (client, input) => { | ||
| const fieldName = requestedFieldName(input); | ||
| const replacement = buildFieldDefinition(input); | ||
| const dropResult = await dropField(client, input); | ||
| await waitForCollectionCondition(client, { | ||
| collection: input.collection, | ||
| fieldMissing: fieldName, | ||
| timeoutMs: input.timeoutMs, | ||
| intervalMs: input.intervalMs | ||
| }); | ||
| try { | ||
| await api(client).patch(collectionPath(input.collection), { | ||
| fields: [replacement] | ||
| }); | ||
| } catch (error) { | ||
| throw new Error( | ||
| [ | ||
| error instanceof Error ? error.message : String(error), | ||
| "", | ||
| "The original field was dropped, but adding the replacement failed.", | ||
| `Recover with: tsk collections.fields.add --collection ${input.collection} --input field.json` | ||
| ].join("\n") | ||
| ); | ||
| } | ||
| const waitResult = await waitForCollectionCondition(client, { | ||
| collection: input.collection, | ||
| fieldPresent: fieldName, | ||
| timeoutMs: input.timeoutMs, | ||
| intervalMs: input.intervalMs | ||
| }); | ||
| const collection = await retrieveCollection(client, input.collection); | ||
| return { | ||
| ok: true, | ||
| collection: input.collection, | ||
| field: fieldName, | ||
| before: "before" in dropResult ? dropResult.before : void 0, | ||
| after: findField(collection, fieldName), | ||
| wait: waitResult | ||
| }; | ||
| } | ||
| }, | ||
| { | ||
| name: "collections.delete", | ||
| summary: "Delete a collection", | ||
| category: "collections", | ||
| input: z5.object({ collection: z5.string() }), | ||
| execute: async (client, input) => api(client).delete(collectionPath(input.collection)) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/conversations.ts | ||
| import { z as z6 } from "zod"; | ||
| var conversationOperations = [ | ||
| { | ||
| name: "conversations.models.list", | ||
| summary: "List conversation models", | ||
| category: "conversations", | ||
| input: z6.object({}), | ||
| execute: async (client) => api(client).get("/conversations/models") | ||
| }, | ||
| { | ||
| name: "conversations.models.create", | ||
| summary: "Create a conversation model", | ||
| category: "conversations", | ||
| input: z6.object({ value: z6.record(z6.unknown()) }), | ||
| execute: async (client, input) => api(client).post("/conversations/models", input.value) | ||
| }, | ||
| { | ||
| name: "conversations.models.retrieve", | ||
| summary: "Retrieve a conversation model", | ||
| category: "conversations", | ||
| input: z6.object({ id: z6.string() }), | ||
| execute: async (client, input) => api(client).get(`/conversations/models/${enc(input.id)}`) | ||
| }, | ||
| { | ||
| name: "conversations.models.delete", | ||
| summary: "Delete a conversation model", | ||
| category: "conversations", | ||
| input: z6.object({ id: z6.string() }), | ||
| execute: async (client, input) => api(client).delete(`/conversations/models/${enc(input.id)}`) | ||
| }, | ||
| { | ||
| name: "conversations.history.retrieve", | ||
| summary: "Retrieve conversation history", | ||
| category: "conversations", | ||
| input: z6.object({ conversation_id: z6.string().optional() }), | ||
| execute: async (client, input) => api(client).get( | ||
| "/conversations/history", | ||
| input.conversation_id ? { conversation_id: input.conversation_id } : void 0 | ||
| ) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/curation-sets.ts | ||
| import { z as z7 } from "zod"; | ||
| function base(name) { | ||
| return name ? `/curation_sets/${enc(name)}` : "/curation_sets"; | ||
| } | ||
| function itemPath(name, id) { | ||
| const path = `${base(name)}/items`; | ||
| return id ? `${path}/${enc(id)}` : path; | ||
| } | ||
| var curationSetOperations = [ | ||
| { | ||
| name: "curation_sets.list", | ||
| summary: "List global curation sets", | ||
| category: "curations", | ||
| input: z7.object({}), | ||
| execute: async (client) => api(client).get(base()) | ||
| }, | ||
| { | ||
| name: "curation_sets.upsert", | ||
| summary: "Create or update a global curation set", | ||
| category: "curations", | ||
| input: z7.object({ | ||
| name: z7.string(), | ||
| value: z7.object({ items: z7.array(z7.record(z7.unknown())) }) | ||
| }), | ||
| execute: async (client, input) => api(client).put(base(input.name), input.value) | ||
| }, | ||
| { | ||
| name: "curation_sets.retrieve", | ||
| summary: "Retrieve a global curation set", | ||
| category: "curations", | ||
| input: z7.object({ name: z7.string() }), | ||
| execute: async (client, input) => api(client).get(base(input.name)) | ||
| }, | ||
| { | ||
| name: "curation_sets.delete", | ||
| summary: "Delete a global curation set", | ||
| category: "curations", | ||
| input: z7.object({ name: z7.string() }), | ||
| execute: async (client, input) => api(client).delete(base(input.name)) | ||
| }, | ||
| { | ||
| name: "curation_sets.items.list", | ||
| summary: "List items in a global curation set", | ||
| category: "curations", | ||
| input: z7.object({ name: z7.string() }), | ||
| execute: async (client, input) => api(client).get(itemPath(input.name)) | ||
| }, | ||
| { | ||
| name: "curation_sets.items.upsert", | ||
| summary: "Create or update an item in a global curation set", | ||
| category: "curations", | ||
| input: z7.object({ | ||
| name: z7.string(), | ||
| id: z7.string(), | ||
| value: z7.record(z7.unknown()) | ||
| }), | ||
| execute: async (client, input) => api(client).put(itemPath(input.name, input.id), input.value) | ||
| }, | ||
| { | ||
| name: "curation_sets.items.retrieve", | ||
| summary: "Retrieve an item in a global curation set", | ||
| category: "curations", | ||
| input: z7.object({ name: z7.string(), id: z7.string() }), | ||
| execute: async (client, input) => api(client).get(itemPath(input.name, input.id)) | ||
| }, | ||
| { | ||
| name: "curation_sets.items.delete", | ||
| summary: "Delete an item in a global curation set", | ||
| category: "curations", | ||
| input: z7.object({ name: z7.string(), id: z7.string() }), | ||
| execute: async (client, input) => api(client).delete(itemPath(input.name, input.id)) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/documents.ts | ||
| import { z as z8 } from "zod"; | ||
| var documentSchema = z8.record(z8.unknown()); | ||
| var idsSchema = z8.array(z8.string().min(1)).min(1); | ||
| var searchParams = z8.record( | ||
| z8.union([ | ||
| z8.string(), | ||
| z8.number(), | ||
| z8.boolean(), | ||
| z8.array(z8.string()), | ||
| z8.array(z8.number()) | ||
| ]) | ||
| ); | ||
| var documentOperations = [ | ||
| { | ||
| name: "documents.index", | ||
| summary: "Index a document", | ||
| category: "documents", | ||
| input: z8.object({ collection: z8.string(), document: documentSchema }), | ||
| execute: async (client, input) => api(client).post( | ||
| `${collectionPath(input.collection)}/documents`, | ||
| input.document | ||
| ) | ||
| }, | ||
| { | ||
| name: "documents.upsert", | ||
| summary: "Upsert a document", | ||
| category: "documents", | ||
| input: z8.object({ collection: z8.string(), document: documentSchema }), | ||
| execute: async (client, input) => api(client).post( | ||
| `${collectionPath(input.collection)}/documents`, | ||
| input.document, | ||
| { action: "upsert" } | ||
| ) | ||
| }, | ||
| { | ||
| name: "documents.get", | ||
| summary: "Get a document by id", | ||
| category: "documents", | ||
| input: z8.object({ collection: z8.string(), id: z8.string() }), | ||
| execute: async (client, input) => api(client).get( | ||
| `${collectionPath(input.collection)}/documents/${enc(input.id)}` | ||
| ) | ||
| }, | ||
| { | ||
| name: "documents.get_many", | ||
| summary: "Get multiple documents by id", | ||
| category: "documents", | ||
| input: z8.object({ collection: z8.string(), ids: idsSchema }), | ||
| execute: async (client, input) => { | ||
| const request = api(client); | ||
| return Promise.all( | ||
| input.ids.map( | ||
| (id) => request.get( | ||
| `${collectionPath(input.collection)}/documents/${enc(id)}` | ||
| ) | ||
| ) | ||
| ); | ||
| } | ||
| }, | ||
| { | ||
| name: "documents.update", | ||
| summary: "Update a document by id", | ||
| category: "documents", | ||
| input: z8.object({ | ||
| collection: z8.string(), | ||
| id: z8.string(), | ||
| document: documentSchema | ||
| }), | ||
| execute: async (client, input) => api(client).patch( | ||
| `${collectionPath(input.collection)}/documents/${enc(input.id)}`, | ||
| input.document | ||
| ) | ||
| }, | ||
| { | ||
| name: "documents.delete", | ||
| summary: "Delete a document by id", | ||
| category: "documents", | ||
| input: z8.object({ collection: z8.string(), id: z8.string() }), | ||
| execute: async (client, input) => api(client).delete( | ||
| `${collectionPath(input.collection)}/documents/${enc(input.id)}` | ||
| ) | ||
| }, | ||
| { | ||
| name: "documents.import", | ||
| summary: "Import documents into a collection", | ||
| category: "documents", | ||
| input: z8.object({ | ||
| collection: z8.string(), | ||
| documents: z8.union([z8.string(), z8.array(documentSchema)]), | ||
| action: z8.enum(["create", "upsert", "update", "emplace"]).optional() | ||
| }), | ||
| execute: async (client, input) => api(client).post( | ||
| `${collectionPath(input.collection)}/documents/import`, | ||
| Array.isArray(input.documents) ? input.documents.map((doc) => JSON.stringify(doc)).join("\n") : input.documents, | ||
| input.action ? { action: input.action } : void 0 | ||
| ) | ||
| }, | ||
| { | ||
| name: "documents.export", | ||
| summary: "Export documents from a collection", | ||
| category: "documents", | ||
| input: z8.object({ | ||
| collection: z8.string(), | ||
| params: searchParams.optional() | ||
| }), | ||
| execute: async (client, input) => api(client).get( | ||
| `${collectionPath(input.collection)}/documents/export`, | ||
| input.params | ||
| ) | ||
| }, | ||
| { | ||
| name: "documents.search", | ||
| summary: "Search within a collection", | ||
| category: "documents", | ||
| input: z8.object({ collection: z8.string(), params: searchParams }), | ||
| execute: async (client, input) => api(client).get( | ||
| `${collectionPath(input.collection)}/documents/search`, | ||
| input.params | ||
| ) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/keys.ts | ||
| import { z as z9 } from "zod"; | ||
| var keysOperations = [ | ||
| { | ||
| name: "keys.list", | ||
| summary: "List API keys", | ||
| category: "keys", | ||
| input: z9.object({}), | ||
| execute: async (client) => api(client).get("/keys") | ||
| }, | ||
| { | ||
| name: "keys.create", | ||
| summary: "Create an API key", | ||
| category: "keys", | ||
| input: z9.object({ value: z9.record(z9.unknown()) }), | ||
| execute: async (client, input) => api(client).post("/keys", input.value) | ||
| }, | ||
| { | ||
| name: "keys.retrieve", | ||
| summary: "Retrieve an API key", | ||
| category: "keys", | ||
| input: z9.object({ id: z9.union([z9.string(), z9.number()]) }), | ||
| execute: async (client, input) => api(client).get(`/keys/${enc(String(input.id))}`) | ||
| }, | ||
| { | ||
| name: "keys.delete", | ||
| summary: "Delete an API key", | ||
| category: "keys", | ||
| input: z9.object({ id: z9.union([z9.string(), z9.number()]) }), | ||
| execute: async (client, input) => api(client).delete(`/keys/${enc(String(input.id))}`) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/nl-search-models.ts | ||
| import { z as z10 } from "zod"; | ||
| var modelConfigSchema = z10.object({ | ||
| id: z10.string().optional(), | ||
| model_name: z10.string().optional(), | ||
| api_key: z10.string().optional(), | ||
| api_url: z10.string().url().optional(), | ||
| max_bytes: z10.number().int().positive().optional(), | ||
| temperature: z10.number().optional(), | ||
| system_prompt: z10.string().optional(), | ||
| top_p: z10.number().optional(), | ||
| top_k: z10.number().int().optional(), | ||
| stop_sequences: z10.array(z10.string()).optional(), | ||
| api_version: z10.string().optional(), | ||
| project_id: z10.string().optional(), | ||
| access_token: z10.string().optional(), | ||
| refresh_token: z10.string().optional(), | ||
| client_id: z10.string().optional(), | ||
| client_secret: z10.string().optional(), | ||
| region: z10.string().optional(), | ||
| max_output_tokens: z10.number().int().positive().optional(), | ||
| account_id: z10.string().optional() | ||
| }).passthrough(); | ||
| function modelPath(id) { | ||
| return `/nl_search_models/${enc(id)}`; | ||
| } | ||
| var nlSearchModelOperations = [ | ||
| { | ||
| name: "nl_search_models.list", | ||
| summary: "List natural language search models", | ||
| category: "nl_search_models", | ||
| input: z10.object({}), | ||
| execute: async (client) => api(client).get("/nl_search_models") | ||
| }, | ||
| { | ||
| name: "nl_search_models.create", | ||
| summary: "Create a natural language search model", | ||
| category: "nl_search_models", | ||
| input: z10.object({ value: modelConfigSchema }), | ||
| execute: async (client, input) => api(client).post("/nl_search_models", input.value) | ||
| }, | ||
| { | ||
| name: "nl_search_models.retrieve", | ||
| summary: "Retrieve a natural language search model", | ||
| category: "nl_search_models", | ||
| input: z10.object({ id: z10.string().min(1) }), | ||
| execute: async (client, input) => api(client).get(modelPath(input.id)) | ||
| }, | ||
| { | ||
| name: "nl_search_models.update", | ||
| summary: "Update a natural language search model", | ||
| category: "nl_search_models", | ||
| input: z10.object({ id: z10.string().min(1), value: modelConfigSchema }), | ||
| execute: async (client, input) => api(client).put(modelPath(input.id), input.value) | ||
| }, | ||
| { | ||
| name: "nl_search_models.delete", | ||
| summary: "Delete a natural language search model", | ||
| category: "nl_search_models", | ||
| input: z10.object({ id: z10.string().min(1) }), | ||
| execute: async (client, input) => api(client).delete(modelPath(input.id)) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/overrides.ts | ||
| import { z as z11 } from "zod"; | ||
| var overridesOperations = [ | ||
| { | ||
| name: "overrides.list", | ||
| summary: "List overrides", | ||
| category: "overrides", | ||
| input: z11.object({ collection: z11.string() }), | ||
| execute: async (client, input) => api(client).get(`${collectionPath(input.collection)}/overrides`) | ||
| }, | ||
| { | ||
| name: "overrides.create", | ||
| summary: "Create or upsert a override", | ||
| category: "overrides", | ||
| input: z11.object({ | ||
| collection: z11.string(), | ||
| name: z11.string(), | ||
| value: z11.record(z11.unknown()) | ||
| }), | ||
| execute: async (client, input) => api(client).put( | ||
| `${collectionPath(input.collection)}/overrides/${enc(input.name)}`, | ||
| input.value | ||
| ) | ||
| }, | ||
| { | ||
| name: "overrides.retrieve", | ||
| summary: "Retrieve a override", | ||
| category: "overrides", | ||
| input: z11.object({ collection: z11.string(), name: z11.string() }), | ||
| execute: async (client, input) => api(client).get( | ||
| `${collectionPath(input.collection)}/overrides/${enc(input.name)}` | ||
| ) | ||
| }, | ||
| { | ||
| name: "overrides.delete", | ||
| summary: "Delete a override", | ||
| category: "overrides", | ||
| input: z11.object({ collection: z11.string(), name: z11.string() }), | ||
| execute: async (client, input) => api(client).delete( | ||
| `${collectionPath(input.collection)}/overrides/${enc(input.name)}` | ||
| ) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/presets.ts | ||
| import { z as z12 } from "zod"; | ||
| var presetsOperations = [ | ||
| { | ||
| name: "presets.list", | ||
| summary: "List presets", | ||
| category: "presets", | ||
| input: z12.object({}), | ||
| execute: async (client) => api(client).get("/presets") | ||
| }, | ||
| { | ||
| name: "presets.create", | ||
| summary: "Create or upsert a preset", | ||
| category: "presets", | ||
| input: z12.object({ | ||
| name: z12.string(), | ||
| value: z12.record(z12.unknown()) | ||
| }), | ||
| execute: async (client, input) => api(client).put(`/presets/${enc(input.name)}`, { | ||
| value: input.value | ||
| }) | ||
| }, | ||
| { | ||
| name: "presets.retrieve", | ||
| summary: "Retrieve a preset", | ||
| category: "presets", | ||
| input: z12.object({ name: z12.string() }), | ||
| execute: async (client, input) => api(client).get(`/presets/${enc(input.name)}`) | ||
| }, | ||
| { | ||
| name: "presets.delete", | ||
| summary: "Delete a preset", | ||
| category: "presets", | ||
| input: z12.object({ name: z12.string() }), | ||
| execute: async (client, input) => api(client).delete(`/presets/${enc(input.name)}`) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/search.ts | ||
| import { z as z13 } from "zod"; | ||
| var searchParams2 = z13.record(z13.unknown()); | ||
| var facetBySchema = z13.union([z13.string().min(1), z13.array(z13.string().min(1))]); | ||
| function withoutUndefined(params) { | ||
| return Object.fromEntries( | ||
| Object.entries(params).filter(([, value]) => value !== void 0) | ||
| ); | ||
| } | ||
| var searchOperations = [ | ||
| { | ||
| name: "search", | ||
| summary: "Search a collection", | ||
| category: "search", | ||
| input: z13.object({ collection: z13.string(), params: searchParams2 }), | ||
| execute: async (client, input) => api(client).get( | ||
| `${collectionPath(input.collection)}/documents/search`, | ||
| input.params | ||
| ) | ||
| }, | ||
| { | ||
| name: "multi_search", | ||
| summary: "Run a Typesense multi-search", | ||
| category: "search", | ||
| input: z13.object({ | ||
| searches: z13.array(searchParams2), | ||
| commonParams: searchParams2.optional() | ||
| }), | ||
| execute: async (client, input) => api(client).post( | ||
| "/multi_search", | ||
| { searches: input.searches }, | ||
| input.commonParams | ||
| ) | ||
| }, | ||
| { | ||
| name: "search.facets", | ||
| summary: "Explore facet counts for a collection", | ||
| category: "search", | ||
| input: z13.object({ | ||
| collection: z13.string(), | ||
| facetBy: facetBySchema, | ||
| q: z13.string().optional().default("*"), | ||
| queryBy: z13.string().optional(), | ||
| filterBy: z13.string().optional(), | ||
| maxFacetValues: z13.number().int().positive().optional(), | ||
| perPage: z13.number().int().nonnegative().optional().default(0) | ||
| }), | ||
| execute: async (client, input) => api(client).get( | ||
| `${collectionPath(input.collection)}/documents/search`, | ||
| withoutUndefined({ | ||
| q: input.q, | ||
| query_by: input.queryBy, | ||
| filter_by: input.filterBy, | ||
| facet_by: Array.isArray(input.facetBy) ? input.facetBy.join(",") : input.facetBy, | ||
| max_facet_values: input.maxFacetValues, | ||
| per_page: input.perPage | ||
| }) | ||
| ) | ||
| }, | ||
| { | ||
| name: "search.suggestions", | ||
| summary: "Fetch prefix search suggestions from a collection", | ||
| category: "search", | ||
| input: z13.object({ | ||
| collection: z13.string(), | ||
| q: z13.string(), | ||
| queryBy: z13.string(), | ||
| filterBy: z13.string().optional(), | ||
| includeFields: z13.union([z13.string(), z13.array(z13.string())]).optional(), | ||
| limit: z13.number().int().positive().max(50).optional().default(5), | ||
| prefix: z13.boolean().optional().default(true) | ||
| }), | ||
| execute: async (client, input) => api(client).get( | ||
| `${collectionPath(input.collection)}/documents/search`, | ||
| withoutUndefined({ | ||
| q: input.q, | ||
| query_by: input.queryBy, | ||
| filter_by: input.filterBy, | ||
| include_fields: Array.isArray(input.includeFields) ? input.includeFields.join(",") : input.includeFields, | ||
| per_page: input.limit, | ||
| prefix: input.prefix | ||
| }) | ||
| ) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/stemming.ts | ||
| import { z as z14 } from "zod"; | ||
| var wordMappingSchema = z14.object({ | ||
| word: z14.string().min(1), | ||
| root: z14.string().min(1) | ||
| }); | ||
| var stemmingOperations = [ | ||
| { | ||
| name: "stemming.dictionaries.list", | ||
| summary: "List stemming dictionaries", | ||
| category: "stemming", | ||
| input: z14.object({}), | ||
| execute: async (client) => api(client).get("/stemming/dictionaries") | ||
| }, | ||
| { | ||
| name: "stemming.dictionaries.retrieve", | ||
| summary: "Retrieve a stemming dictionary", | ||
| category: "stemming", | ||
| input: z14.object({ id: z14.string().min(1) }), | ||
| execute: async (client, input) => api(client).get(`/stemming/dictionaries/${enc(input.id)}`) | ||
| }, | ||
| { | ||
| name: "stemming.dictionaries.import", | ||
| summary: "Import or replace a stemming dictionary from word mappings", | ||
| category: "stemming", | ||
| input: z14.object({ | ||
| id: z14.string().min(1), | ||
| words: z14.union([z14.string().min(1), z14.array(wordMappingSchema).min(1)]) | ||
| }), | ||
| execute: async (client, input) => api(client).post( | ||
| "/stemming/dictionaries/import", | ||
| Array.isArray(input.words) ? input.words.map( | ||
| (mapping) => JSON.stringify(mapping) | ||
| ).join("\n") : input.words, | ||
| { id: input.id } | ||
| ) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/stopwords.ts | ||
| import { z as z15 } from "zod"; | ||
| var stopwordsOperations = [ | ||
| { | ||
| name: "stopwords.list", | ||
| summary: "List stopwords", | ||
| category: "stopwords", | ||
| input: z15.object({}), | ||
| execute: async (client) => api(client).get("/stopwords") | ||
| }, | ||
| { | ||
| name: "stopwords.create", | ||
| summary: "Create or upsert a stopword", | ||
| category: "stopwords", | ||
| input: z15.object({ | ||
| name: z15.string(), | ||
| value: z15.record(z15.unknown()) | ||
| }), | ||
| execute: async (client, input) => api(client).put(`/stopwords/${enc(input.name)}`, input.value) | ||
| }, | ||
| { | ||
| name: "stopwords.retrieve", | ||
| summary: "Retrieve a stopword", | ||
| category: "stopwords", | ||
| input: z15.object({ name: z15.string() }), | ||
| execute: async (client, input) => api(client).get(`/stopwords/${enc(input.name)}`) | ||
| }, | ||
| { | ||
| name: "stopwords.delete", | ||
| summary: "Delete a stopword", | ||
| category: "stopwords", | ||
| input: z15.object({ name: z15.string() }), | ||
| execute: async (client, input) => api(client).delete(`/stopwords/${enc(input.name)}`) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/synonym-sets.ts | ||
| import { z as z16 } from "zod"; | ||
| function base2(name) { | ||
| return name ? `/synonym_sets/${enc(name)}` : "/synonym_sets"; | ||
| } | ||
| function itemPath2(name, id) { | ||
| const path = `${base2(name)}/items`; | ||
| return id ? `${path}/${enc(id)}` : path; | ||
| } | ||
| var synonymSetOperations = [ | ||
| { | ||
| name: "synonym_sets.list", | ||
| summary: "List global synonym sets", | ||
| category: "synonyms", | ||
| input: z16.object({}), | ||
| execute: async (client) => api(client).get(base2()) | ||
| }, | ||
| { | ||
| name: "synonym_sets.create", | ||
| summary: "Create or upsert a global synonym set", | ||
| category: "synonyms", | ||
| input: z16.object({ | ||
| name: z16.string(), | ||
| value: z16.object({ | ||
| items: z16.array(z16.record(z16.unknown())) | ||
| }) | ||
| }), | ||
| execute: async (client, input) => api(client).put(base2(input.name), input.value) | ||
| }, | ||
| { | ||
| name: "synonym_sets.retrieve", | ||
| summary: "Retrieve a global synonym set", | ||
| category: "synonyms", | ||
| input: z16.object({ name: z16.string() }), | ||
| execute: async (client, input) => api(client).get(base2(input.name)) | ||
| }, | ||
| { | ||
| name: "synonym_sets.delete", | ||
| summary: "Delete a global synonym set", | ||
| category: "synonyms", | ||
| input: z16.object({ name: z16.string() }), | ||
| execute: async (client, input) => api(client).delete(base2(input.name)) | ||
| }, | ||
| { | ||
| name: "synonym_sets.items.list", | ||
| summary: "List items in a global synonym set", | ||
| category: "synonyms", | ||
| input: z16.object({ name: z16.string() }), | ||
| execute: async (client, input) => api(client).get(itemPath2(input.name)) | ||
| }, | ||
| { | ||
| name: "synonym_sets.items.create", | ||
| summary: "Create or upsert an item in a global synonym set", | ||
| category: "synonyms", | ||
| input: z16.object({ | ||
| name: z16.string(), | ||
| id: z16.string(), | ||
| value: z16.record(z16.unknown()) | ||
| }), | ||
| execute: async (client, input) => api(client).put(itemPath2(input.name, input.id), input.value) | ||
| }, | ||
| { | ||
| name: "synonym_sets.items.retrieve", | ||
| summary: "Retrieve an item in a global synonym set", | ||
| category: "synonyms", | ||
| input: z16.object({ name: z16.string(), id: z16.string() }), | ||
| execute: async (client, input) => api(client).get(itemPath2(input.name, input.id)) | ||
| }, | ||
| { | ||
| name: "synonym_sets.items.delete", | ||
| summary: "Delete an item in a global synonym set", | ||
| category: "synonyms", | ||
| input: z16.object({ name: z16.string(), id: z16.string() }), | ||
| execute: async (client, input) => api(client).delete(itemPath2(input.name, input.id)) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/synonyms.ts | ||
| import { z as z17 } from "zod"; | ||
| function isNotFound(error) { | ||
| if (typeof error !== "object" || error === null) return false; | ||
| const { httpStatus, status } = error; | ||
| return httpStatus === 404 || status === 404; | ||
| } | ||
| function synonymSetNames(collection) { | ||
| return Array.isArray(collection.synonym_sets) ? collection.synonym_sets.filter( | ||
| (name) => typeof name === "string" | ||
| ) : []; | ||
| } | ||
| function globalSynonymGuidance(collection, sets) { | ||
| return [ | ||
| `Collection-level synonyms are unavailable for ${collection}.`, | ||
| sets.length > 0 ? `This collection is linked to global synonym sets: ${sets.join(", ")}.` : "This Typesense version may use global synonym sets.", | ||
| "Use synonym_sets.list to inspect global synonym sets:", | ||
| "tsk synonym_sets.list --input '{}' --json" | ||
| ].join("\n"); | ||
| } | ||
| var synonymsOperations = [ | ||
| { | ||
| name: "synonyms.list", | ||
| summary: "List synonyms", | ||
| category: "synonyms", | ||
| input: z17.object({ collection: z17.string() }), | ||
| execute: async (client, input) => { | ||
| const request = api(client); | ||
| try { | ||
| return await request.get( | ||
| `${collectionPath(input.collection)}/synonyms` | ||
| ); | ||
| } catch (error) { | ||
| if (!isNotFound(error)) throw error; | ||
| const collection = await request.get( | ||
| collectionPath(input.collection) | ||
| ); | ||
| throw new Error( | ||
| globalSynonymGuidance(input.collection, synonymSetNames(collection)) | ||
| ); | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| name: "synonyms.create", | ||
| summary: "Create or upsert a synonym", | ||
| category: "synonyms", | ||
| input: z17.object({ | ||
| collection: z17.string(), | ||
| name: z17.string(), | ||
| value: z17.record(z17.unknown()) | ||
| }), | ||
| execute: async (client, input) => api(client).put( | ||
| `${collectionPath(input.collection)}/synonyms/${enc(input.name)}`, | ||
| input.value | ||
| ) | ||
| }, | ||
| { | ||
| name: "synonyms.retrieve", | ||
| summary: "Retrieve a synonym", | ||
| category: "synonyms", | ||
| input: z17.object({ collection: z17.string(), name: z17.string() }), | ||
| execute: async (client, input) => api(client).get( | ||
| `${collectionPath(input.collection)}/synonyms/${enc(input.name)}` | ||
| ) | ||
| }, | ||
| { | ||
| name: "synonyms.delete", | ||
| summary: "Delete a synonym", | ||
| category: "synonyms", | ||
| input: z17.object({ collection: z17.string(), name: z17.string() }), | ||
| execute: async (client, input) => api(client).delete( | ||
| `${collectionPath(input.collection)}/synonyms/${enc(input.name)}` | ||
| ) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/system.ts | ||
| import { z as z18 } from "zod"; | ||
| var systemOperations = [ | ||
| { | ||
| name: "operations.schema_changes", | ||
| summary: "List in-progress collection schema changes", | ||
| category: "system", | ||
| input: z18.object({}), | ||
| execute: async (client) => api(client).get("/operations/schema_changes") | ||
| }, | ||
| { | ||
| name: "operations.snapshot", | ||
| summary: "Create a point-in-time server snapshot", | ||
| category: "system", | ||
| input: z18.object({ snapshotPath: z18.string().min(1) }), | ||
| execute: async (client, input) => api(client).post("/operations/snapshot", void 0, { | ||
| snapshot_path: input.snapshotPath | ||
| }) | ||
| }, | ||
| { | ||
| name: "operations.vote", | ||
| summary: "Trigger leader re-election on a follower node", | ||
| category: "system", | ||
| input: z18.object({}), | ||
| execute: async (client) => api(client).post("/operations/vote") | ||
| }, | ||
| { | ||
| name: "operations.cache.clear", | ||
| summary: "Clear cached search responses", | ||
| category: "system", | ||
| input: z18.object({}), | ||
| execute: async (client) => api(client).post("/operations/cache/clear") | ||
| }, | ||
| { | ||
| name: "operations.db.compact", | ||
| summary: "Compact the on-disk Typesense database", | ||
| category: "system", | ||
| input: z18.object({}), | ||
| execute: async (client) => api(client).post("/operations/db/compact") | ||
| }, | ||
| { | ||
| name: "operations.slow_requests.configure", | ||
| summary: "Configure the slow-request logging threshold", | ||
| category: "system", | ||
| input: z18.object({ thresholdMs: z18.number().int().min(-1) }), | ||
| execute: async (client, input) => api(client).post("/config", { | ||
| "log-slow-requests-time-ms": input.thresholdMs | ||
| }) | ||
| }, | ||
| { | ||
| name: "health", | ||
| summary: "Check Typesense cluster health", | ||
| category: "system", | ||
| input: z18.object({}), | ||
| execute: async (client) => api(client).get("/health") | ||
| }, | ||
| { | ||
| name: "metrics", | ||
| summary: "Retrieve Typesense metrics", | ||
| category: "system", | ||
| input: z18.object({}), | ||
| execute: async (client) => api(client).get("/metrics.json") | ||
| }, | ||
| { | ||
| name: "stats", | ||
| summary: "Retrieve Typesense stats", | ||
| category: "system", | ||
| input: z18.object({}), | ||
| execute: async (client) => api(client).get("/stats.json") | ||
| }, | ||
| { | ||
| name: "debug", | ||
| summary: "Retrieve Typesense debug info", | ||
| category: "system", | ||
| input: z18.object({}), | ||
| execute: async (client) => api(client).get("/debug") | ||
| } | ||
| ]; | ||
| // ../core/src/operations/index.ts | ||
| var operations = [ | ||
| ...collectionOperations, | ||
| ...documentOperations, | ||
| ...searchOperations, | ||
| ...aliasesOperations, | ||
| ...synonymsOperations, | ||
| ...synonymSetOperations, | ||
| ...curationSetOperations, | ||
| ...overridesOperations, | ||
| ...keysOperations, | ||
| ...analyticsOperations, | ||
| ...presetsOperations, | ||
| ...stopwordsOperations, | ||
| ...stemmingOperations, | ||
| ...conversationOperations, | ||
| ...nlSearchModelOperations, | ||
| ...apiOperations, | ||
| ...systemOperations | ||
| ]; | ||
| // src/server.ts | ||
| import { z as z19 } from "zod"; | ||
| // package.json | ||
| var package_default = { | ||
| name: "@typesensekit/mcp", | ||
| version: "1.3.0", | ||
| type: "module", | ||
| main: "dist/server.js", | ||
| types: "dist/server.d.ts", | ||
| bin: { | ||
| "typesensekit-mcp": "dist/cli.js", | ||
| "typesensekit-mcp-http": "dist/http.js" | ||
| }, | ||
| exports: { | ||
| ".": { | ||
| types: "./dist/server.d.ts", | ||
| import: "./dist/server.js" | ||
| } | ||
| }, | ||
| files: [ | ||
| "dist", | ||
| "README.md" | ||
| ], | ||
| scripts: { | ||
| build: "tsup", | ||
| dev: "tsup --watch", | ||
| typecheck: "tsc --noEmit", | ||
| test: "vitest run", | ||
| "smoke:stdio": "node scripts/smoke-stdio.mjs" | ||
| }, | ||
| dependencies: { | ||
| "@modelcontextprotocol/sdk": "^1.29.0", | ||
| zod: "^3.25.76", | ||
| typesense: "^3.0.6" | ||
| }, | ||
| devDependencies: { | ||
| tsup: "^8.5.1", | ||
| "@types/node": "^22.10.0", | ||
| "@typesensekit/core": "workspace:*" | ||
| }, | ||
| publishConfig: { | ||
| access: "public" | ||
| }, | ||
| description: "MCP stdio server exposing Typesense API operations as tools.", | ||
| license: "MIT", | ||
| author: "Akshit Kr Nagpal", | ||
| repository: { | ||
| type: "git", | ||
| url: "git+https://github.com/akshitkrnagpal/typesensekit.git", | ||
| directory: "packages/mcp" | ||
| }, | ||
| bugs: { | ||
| url: "https://github.com/akshitkrnagpal/typesensekit/issues" | ||
| }, | ||
| homepage: "https://github.com/akshitkrnagpal/typesensekit#readme", | ||
| keywords: [ | ||
| "typesense", | ||
| "cli", | ||
| "mcp", | ||
| "model-context-protocol", | ||
| "search" | ||
| ] | ||
| }; | ||
| // src/audit.ts | ||
| var disabledLogger = { record: () => void 0 }; | ||
| function createMcpAuditLogger(env = process.env, sink = console.error) { | ||
| const enabled = ["1", "true", "yes", "on"].includes( | ||
| (env.TYPESENSEKIT_MCP_AUDIT_LOG ?? "").toLowerCase() | ||
| ); | ||
| if (!enabled) return disabledLogger; | ||
| return { | ||
| record(event) { | ||
| sink(JSON.stringify({ type: "typesensekit.mcp.tool", ...event })); | ||
| } | ||
| }; | ||
| } | ||
| // src/read-only.ts | ||
| var READ_ONLY_OPERATION_NAMES = /* @__PURE__ */ new Set([ | ||
| "aliases.list", | ||
| "aliases.retrieve", | ||
| "analytics.events.list", | ||
| "analytics.rules.list", | ||
| "analytics.rules.retrieve", | ||
| "analytics.status", | ||
| "collections.list", | ||
| "collections.retrieve", | ||
| "collections.wait", | ||
| "conversations.history.retrieve", | ||
| "conversations.models.list", | ||
| "conversations.models.retrieve", | ||
| "curation_sets.items.list", | ||
| "curation_sets.items.retrieve", | ||
| "curation_sets.list", | ||
| "curation_sets.retrieve", | ||
| "debug", | ||
| "documents.export", | ||
| "documents.get", | ||
| "documents.get_many", | ||
| "documents.search", | ||
| "health", | ||
| "metrics", | ||
| "multi_search", | ||
| "nl_search_models.list", | ||
| "nl_search_models.retrieve", | ||
| "overrides.list", | ||
| "overrides.retrieve", | ||
| "operations.schema_changes", | ||
| "presets.list", | ||
| "presets.retrieve", | ||
| "search", | ||
| "search.facets", | ||
| "search.suggestions", | ||
| "stats", | ||
| "stemming.dictionaries.list", | ||
| "stemming.dictionaries.retrieve", | ||
| "stopwords.list", | ||
| "stopwords.retrieve", | ||
| "synonym_sets.items.list", | ||
| "synonym_sets.items.retrieve", | ||
| "synonym_sets.list", | ||
| "synonym_sets.retrieve", | ||
| "synonyms.list", | ||
| "synonyms.retrieve" | ||
| ]); | ||
| function isReadOnlyOperation(operation) { | ||
| return READ_ONLY_OPERATION_NAMES.has(operation.name); | ||
| } | ||
| function filterMcpOperations(operations2, readOnly) { | ||
| return readOnly ? operations2.filter(isReadOnlyOperation) : operations2; | ||
| } | ||
| function readOnlyFromEnv(value) { | ||
| if (value === void 0) return true; | ||
| return !["0", "false", "no", "off"].includes(value.toLowerCase()); | ||
| } | ||
| // src/env.ts | ||
| function readEnvConfig() { | ||
| return serverConfigSchema.parse({ | ||
| url: process.env.TYPESENSE_URL, | ||
| apiKey: process.env.TYPESENSE_API_KEY, | ||
| connectionTimeoutSeconds: process.env.TYPESENSE_CONNECTION_TIMEOUT_SECONDS ? Number(process.env.TYPESENSE_CONNECTION_TIMEOUT_SECONDS) : void 0 | ||
| }); | ||
| } | ||
| function readMcpOptions() { | ||
| return { | ||
| readOnly: readOnlyFromEnv(process.env.TYPESENSEKIT_READ_ONLY) | ||
| }; | ||
| } | ||
| // src/execution.ts | ||
| var DEFAULT_CONFIG = { | ||
| timeoutMs: 3e4, | ||
| maxConcurrency: 8, | ||
| rateLimitPerMinute: 120, | ||
| maxResponseBytes: 1024 * 1024 | ||
| }; | ||
| var McpExecutionLimitError = class extends Error { | ||
| }; | ||
| function positiveInteger(value, fallback, name) { | ||
| if (!value) return fallback; | ||
| const parsed = Number(value); | ||
| if (!Number.isInteger(parsed) || parsed <= 0) { | ||
| throw new Error(`Invalid ${name}: ${value}`); | ||
| } | ||
| return parsed; | ||
| } | ||
| function readMcpExecutionConfig(env = process.env) { | ||
| return { | ||
| timeoutMs: positiveInteger( | ||
| env.TYPESENSEKIT_MCP_TOOL_TIMEOUT_MS, | ||
| DEFAULT_CONFIG.timeoutMs, | ||
| "MCP tool timeout" | ||
| ), | ||
| maxConcurrency: positiveInteger( | ||
| env.TYPESENSEKIT_MCP_MAX_CONCURRENCY, | ||
| DEFAULT_CONFIG.maxConcurrency, | ||
| "MCP concurrency limit" | ||
| ), | ||
| rateLimitPerMinute: positiveInteger( | ||
| env.TYPESENSEKIT_MCP_RATE_LIMIT_PER_MINUTE, | ||
| DEFAULT_CONFIG.rateLimitPerMinute, | ||
| "MCP rate limit" | ||
| ), | ||
| maxResponseBytes: positiveInteger( | ||
| env.TYPESENSEKIT_MCP_MAX_RESPONSE_BYTES, | ||
| DEFAULT_CONFIG.maxResponseBytes, | ||
| "MCP response limit" | ||
| ) | ||
| }; | ||
| } | ||
| var McpExecutionController = class { | ||
| config; | ||
| active = 0; | ||
| starts = []; | ||
| clock; | ||
| constructor(config, clock = Date.now) { | ||
| this.config = config; | ||
| this.clock = clock; | ||
| } | ||
| async run(task) { | ||
| const now = this.clock(); | ||
| this.starts = this.starts.filter((started) => now - started < 6e4); | ||
| if (this.starts.length >= this.config.rateLimitPerMinute) { | ||
| throw new McpExecutionLimitError( | ||
| `MCP tool rate limit exceeded (${this.config.rateLimitPerMinute} calls per minute).` | ||
| ); | ||
| } | ||
| if (this.active >= this.config.maxConcurrency) { | ||
| throw new McpExecutionLimitError( | ||
| `MCP tool concurrency limit exceeded (${this.config.maxConcurrency} active calls).` | ||
| ); | ||
| } | ||
| this.starts.push(now); | ||
| this.active += 1; | ||
| let timeout; | ||
| try { | ||
| return await Promise.race([ | ||
| task(), | ||
| new Promise((_resolve, reject) => { | ||
| timeout = setTimeout( | ||
| () => reject( | ||
| new McpExecutionLimitError( | ||
| `MCP tool timed out after ${this.config.timeoutMs}ms.` | ||
| ) | ||
| ), | ||
| this.config.timeoutMs | ||
| ); | ||
| }) | ||
| ]); | ||
| } finally { | ||
| if (timeout) clearTimeout(timeout); | ||
| this.active -= 1; | ||
| } | ||
| } | ||
| serialize(value) { | ||
| const text = JSON.stringify(value, null, 2); | ||
| const bytes = Buffer.byteLength(text); | ||
| if (bytes > this.config.maxResponseBytes) { | ||
| throw new McpExecutionLimitError( | ||
| `MCP tool response is ${bytes} bytes, exceeding the ${this.config.maxResponseBytes}-byte limit. Narrow the request or increase TYPESENSEKIT_MCP_MAX_RESPONSE_BYTES.` | ||
| ); | ||
| } | ||
| return text; | ||
| } | ||
| }; | ||
| var sharedController; | ||
| function sharedMcpExecutionController() { | ||
| sharedController ??= new McpExecutionController(readMcpExecutionConfig()); | ||
| return sharedController; | ||
| } | ||
| // src/resources.ts | ||
| import { | ||
| ResourceTemplate | ||
| } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| function jsonContents(uri, value) { | ||
| return { | ||
| contents: [ | ||
| { | ||
| uri, | ||
| mimeType: "application/json", | ||
| text: JSON.stringify(redactSecrets(value), null, 2) | ||
| } | ||
| ] | ||
| }; | ||
| } | ||
| function textContents(uri, value) { | ||
| return { | ||
| contents: [{ uri, mimeType: "text/plain", text: value }] | ||
| }; | ||
| } | ||
| function operationSummary(operation) { | ||
| return { | ||
| name: operation.name, | ||
| summary: operation.summary, | ||
| category: operation.category, | ||
| readOnly: READ_ONLY_OPERATION_NAMES.has(operation.name) | ||
| }; | ||
| } | ||
| function operationManifest(activeOperations, readOnly) { | ||
| return { | ||
| readOnly, | ||
| operations: activeOperations.map(operationSummary) | ||
| }; | ||
| } | ||
| function singleVariable(value, variable) { | ||
| if (typeof value === "string") return value; | ||
| throw new Error(`Missing ${variable} resource variable`); | ||
| } | ||
| async function readOperationResource(client, operationName, input, uri) { | ||
| const operation = operations.find( | ||
| (candidate) => candidate.name === operationName | ||
| ); | ||
| if (!operation) throw new Error(`${operationName} not found`); | ||
| try { | ||
| const result = await operation.execute( | ||
| client, | ||
| operation.input.parse(input) | ||
| ); | ||
| return jsonContents(uri, result); | ||
| } catch (error) { | ||
| return textContents(uri, formatTypesenseErrorMessage(error)); | ||
| } | ||
| } | ||
| function registerTypesenseResources(server, client, activeOperations, readOnly) { | ||
| server.registerResource( | ||
| "typesensekit-operations", | ||
| "typesensekit://operations", | ||
| { | ||
| title: "TypesenseKit Operations", | ||
| description: "Operations currently exposed by this MCP server.", | ||
| mimeType: "application/json" | ||
| }, | ||
| async (uri) => jsonContents(uri.href, operationManifest(activeOperations, readOnly)) | ||
| ); | ||
| server.registerResource( | ||
| "typesensekit-read-only-tools", | ||
| "typesensekit://read-only-tools", | ||
| { | ||
| title: "TypesenseKit Read-only Tools", | ||
| description: "Operation names included in default read-only MCP mode.", | ||
| mimeType: "application/json" | ||
| }, | ||
| async (uri) => jsonContents(uri.href, { | ||
| operations: [...READ_ONLY_OPERATION_NAMES].sort() | ||
| }) | ||
| ); | ||
| server.registerResource( | ||
| "typesense-collection-schema", | ||
| new ResourceTemplate("typesense://collections/{collection}/schema", { | ||
| list: void 0 | ||
| }), | ||
| { | ||
| title: "Typesense Collection Schema", | ||
| description: "Retrieve a Typesense collection schema by collection name.", | ||
| mimeType: "application/json" | ||
| }, | ||
| async (uri, variables) => readOperationResource( | ||
| client, | ||
| "collections.retrieve", | ||
| { collection: singleVariable(variables.collection, "collection") }, | ||
| uri.href | ||
| ) | ||
| ); | ||
| server.registerResource( | ||
| "typesense-document", | ||
| new ResourceTemplate( | ||
| "typesense://collections/{collection}/documents/{id}", | ||
| { | ||
| list: void 0 | ||
| } | ||
| ), | ||
| { | ||
| title: "Typesense Document", | ||
| description: "Retrieve a Typesense document by collection and document id.", | ||
| mimeType: "application/json" | ||
| }, | ||
| async (uri, variables) => readOperationResource( | ||
| client, | ||
| "documents.get", | ||
| { | ||
| collection: singleVariable(variables.collection, "collection"), | ||
| id: singleVariable(variables.id, "id") | ||
| }, | ||
| uri.href | ||
| ) | ||
| ); | ||
| } | ||
| // src/tool-metadata.ts | ||
| var DESTRUCTIVE_OPERATION_NAMES = /* @__PURE__ */ new Set([ | ||
| "api.call", | ||
| "collections.delete", | ||
| "collections.fields.drop", | ||
| "collections.fields.replace", | ||
| "documents.delete", | ||
| "keys.delete" | ||
| ]); | ||
| function isDestructive(name) { | ||
| return DESTRUCTIVE_OPERATION_NAMES.has(name) || name.endsWith(".delete"); | ||
| } | ||
| function isIdempotent(name, readOnly) { | ||
| return readOnly || name.endsWith(".upsert") || name.endsWith(".update") || name === "collections.wait" || name === "operations.slow_requests.configure"; | ||
| } | ||
| function operationToolAnnotations(operation) { | ||
| const readOnly = READ_ONLY_OPERATION_NAMES.has(operation.name); | ||
| return { | ||
| readOnlyHint: readOnly, | ||
| destructiveHint: isDestructive(operation.name), | ||
| idempotentHint: isIdempotent(operation.name, readOnly), | ||
| openWorldHint: true | ||
| }; | ||
| } | ||
| // src/server.ts | ||
| function toToolShape(input) { | ||
| const objectInput = input; | ||
| return objectInput.shape; | ||
| } | ||
| function createTypesenseMcpServer(options = {}) { | ||
| const server = new McpServer({ | ||
| name: "typesensekit", | ||
| version: package_default.version | ||
| }); | ||
| const client = createClient(readEnvConfig()); | ||
| const mcpOptions = { ...readMcpOptions(), ...options }; | ||
| const execution = options.executionController ?? sharedMcpExecutionController(); | ||
| const audit = options.auditLogger ?? createMcpAuditLogger(); | ||
| const activeOperations = filterMcpOperations(operations, mcpOptions.readOnly); | ||
| registerTypesenseResources( | ||
| server, | ||
| client, | ||
| activeOperations, | ||
| mcpOptions.readOnly | ||
| ); | ||
| for (const operation of activeOperations) { | ||
| server.registerTool( | ||
| operation.name, | ||
| { | ||
| title: operation.name, | ||
| description: operation.summary, | ||
| inputSchema: toToolShape(operation.input), | ||
| outputSchema: { result: z19.unknown() }, | ||
| annotations: operationToolAnnotations(operation) | ||
| }, | ||
| async (args) => { | ||
| const startedAt = Date.now(); | ||
| audit.record({ | ||
| timestamp: new Date(startedAt).toISOString(), | ||
| operation: operation.name, | ||
| outcome: "started" | ||
| }); | ||
| try { | ||
| const input = operation.input.parse(args); | ||
| const result = await execution.run( | ||
| () => operation.execute(client, input) | ||
| ); | ||
| const safeResult = redactSecrets(result); | ||
| audit.record({ | ||
| timestamp: (/* @__PURE__ */ new Date()).toISOString(), | ||
| operation: operation.name, | ||
| outcome: "succeeded", | ||
| durationMs: Date.now() - startedAt | ||
| }); | ||
| return { | ||
| structuredContent: { result: safeResult }, | ||
| content: [ | ||
| { | ||
| type: "text", | ||
| text: execution.serialize(safeResult) | ||
| } | ||
| ] | ||
| }; | ||
| } catch (error) { | ||
| audit.record({ | ||
| timestamp: (/* @__PURE__ */ new Date()).toISOString(), | ||
| operation: operation.name, | ||
| outcome: "failed", | ||
| durationMs: Date.now() - startedAt, | ||
| errorName: error instanceof Error ? error.name : "UnknownError" | ||
| }); | ||
| const message = formatTypesenseErrorMessage(error); | ||
| return { isError: true, content: [{ type: "text", text: message }] }; | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| return server; | ||
| } | ||
| export { | ||
| createTypesenseMcpServer | ||
| }; |
+1
-1
| #!/usr/bin/env node | ||
| import { | ||
| createTypesenseMcpServer | ||
| } from "./chunk-KY4R2JA7.js"; | ||
| } from "./chunk-V6MIDJUV.js"; | ||
@@ -6,0 +6,0 @@ // src/cli.ts |
+154
-64
| #!/usr/bin/env node | ||
| import { | ||
| createTypesenseMcpServer | ||
| } from "./chunk-KY4R2JA7.js"; | ||
| } from "./chunk-V6MIDJUV.js"; | ||
| // src/http.ts | ||
| // src/http-server.ts | ||
| import { createHash, timingSafeEqual } from "crypto"; | ||
| import { | ||
@@ -11,26 +12,98 @@ createServer | ||
| import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; | ||
| var DEFAULT_HOST = "127.0.0.1"; | ||
| var DEFAULT_PORT = 3e3; | ||
| var DEFAULT_PATH = "/mcp"; | ||
| function readPort() { | ||
| const value = process.env.TYPESENSEKIT_MCP_PORT ?? process.env.PORT; | ||
| if (!value) return DEFAULT_PORT; | ||
| const port2 = Number(value); | ||
| if (!Number.isInteger(port2) || port2 <= 0) { | ||
| throw new Error(`Invalid MCP HTTP port: ${value}`); | ||
| var DEFAULT_MAX_BODY_BYTES = 1024 * 1024; | ||
| var BodyTooLargeError = class extends Error { | ||
| }; | ||
| var InvalidJsonError = class extends Error { | ||
| }; | ||
| function positiveInteger(value, fallback, name) { | ||
| if (!value) return fallback; | ||
| const parsed = Number(value); | ||
| if (!Number.isInteger(parsed) || parsed <= 0) { | ||
| throw new Error(`Invalid ${name}: ${value}`); | ||
| } | ||
| return port2; | ||
| return parsed; | ||
| } | ||
| function readPath() { | ||
| const path2 = process.env.TYPESENSEKIT_MCP_PATH ?? DEFAULT_PATH; | ||
| return path2.startsWith("/") ? path2 : `/${path2}`; | ||
| function readBoolean(value) { | ||
| return value !== void 0 && ["1", "true", "yes", "on"].includes(value.toLowerCase()); | ||
| } | ||
| function sendJson(res, statusCode, body) { | ||
| res.writeHead(statusCode, { "content-type": "application/json" }); | ||
| function isLoopback(host) { | ||
| return ["127.0.0.1", "::1", "localhost"].includes(host.toLowerCase()); | ||
| } | ||
| function readHttpConfig(env = process.env) { | ||
| const host = env.TYPESENSEKIT_MCP_HOST ?? DEFAULT_HOST; | ||
| const port = positiveInteger( | ||
| env.TYPESENSEKIT_MCP_PORT ?? env.PORT, | ||
| DEFAULT_PORT, | ||
| "MCP HTTP port" | ||
| ); | ||
| const configuredPath = env.TYPESENSEKIT_MCP_PATH ?? DEFAULT_PATH; | ||
| const path = configuredPath.startsWith("/") ? configuredPath : `/${configuredPath}`; | ||
| const bearerToken = env.TYPESENSEKIT_MCP_BEARER_TOKEN; | ||
| const allowUnauthenticated = readBoolean( | ||
| env.TYPESENSEKIT_MCP_ALLOW_UNAUTHENTICATED | ||
| ); | ||
| const configuredOrigins = (env.TYPESENSEKIT_MCP_ALLOWED_ORIGINS ?? "").split(",").map((origin) => origin.trim()).filter(Boolean); | ||
| const allowedOrigins = /* @__PURE__ */ new Set([ | ||
| `http://localhost:${port}`, | ||
| `http://127.0.0.1:${port}`, | ||
| ...configuredOrigins | ||
| ]); | ||
| if (!isLoopback(host) && !bearerToken && !allowUnauthenticated) { | ||
| throw new Error( | ||
| "Refusing unauthenticated non-loopback MCP HTTP binding. Set TYPESENSEKIT_MCP_BEARER_TOKEN or explicitly trust an authenticating proxy with TYPESENSEKIT_MCP_ALLOW_UNAUTHENTICATED=true." | ||
| ); | ||
| } | ||
| return { | ||
| host, | ||
| port, | ||
| path, | ||
| bearerToken, | ||
| allowedOrigins, | ||
| allowUnauthenticated, | ||
| maxBodyBytes: positiveInteger( | ||
| env.TYPESENSEKIT_MCP_MAX_BODY_BYTES, | ||
| DEFAULT_MAX_BODY_BYTES, | ||
| "MCP HTTP body limit" | ||
| ) | ||
| }; | ||
| } | ||
| function sendJson(res, statusCode, body, headers = {}) { | ||
| res.writeHead(statusCode, { "content-type": "application/json", ...headers }); | ||
| res.end(JSON.stringify(body)); | ||
| } | ||
| function readBody(req) { | ||
| function jsonRpcError(message) { | ||
| return { | ||
| jsonrpc: "2.0", | ||
| error: { code: -32603, message }, | ||
| id: null | ||
| }; | ||
| } | ||
| function secureEqual(left, right) { | ||
| const leftHash = createHash("sha256").update(left).digest(); | ||
| const rightHash = createHash("sha256").update(right).digest(); | ||
| return timingSafeEqual(leftHash, rightHash); | ||
| } | ||
| function authorized(req, bearerToken) { | ||
| if (!bearerToken) return true; | ||
| const header = req.headers.authorization; | ||
| if (!header?.startsWith("Bearer ")) return false; | ||
| return secureEqual(header.slice("Bearer ".length), bearerToken); | ||
| } | ||
| async function readJsonBody(req, maxBodyBytes) { | ||
| const declaredLength = Number(req.headers["content-length"] ?? 0); | ||
| if (declaredLength > maxBodyBytes) throw new BodyTooLargeError(); | ||
| return new Promise((resolve, reject) => { | ||
| let body = ""; | ||
| let bytes = 0; | ||
| let exceeded = false; | ||
| req.setEncoding("utf8"); | ||
| req.on("data", (chunk) => { | ||
| bytes += Buffer.byteLength(chunk); | ||
| if (bytes > maxBodyBytes) { | ||
| exceeded = true; | ||
| return; | ||
| } | ||
| body += chunk; | ||
@@ -40,2 +113,6 @@ }); | ||
| req.on("end", () => { | ||
| if (exceeded) { | ||
| reject(new BodyTooLargeError()); | ||
| return; | ||
| } | ||
| if (!body) { | ||
@@ -47,4 +124,4 @@ resolve(void 0); | ||
| resolve(JSON.parse(body)); | ||
| } catch (error) { | ||
| reject(error); | ||
| } catch { | ||
| reject(new InvalidJsonError()); | ||
| } | ||
@@ -54,10 +131,3 @@ }); | ||
| } | ||
| function jsonRpcError(message) { | ||
| return { | ||
| jsonrpc: "2.0", | ||
| error: { code: -32603, message }, | ||
| id: null | ||
| }; | ||
| } | ||
| async function handleMcpRequest(req, res) { | ||
| async function handleMcpRequest(req, res, maxBodyBytes) { | ||
| const server = createTypesenseMcpServer(); | ||
@@ -67,44 +137,64 @@ const transport = new StreamableHTTPServerTransport({ | ||
| }); | ||
| try { | ||
| const body = await readBody(req); | ||
| await server.connect(transport); | ||
| await transport.handleRequest(req, res, body); | ||
| } finally { | ||
| res.on("close", () => { | ||
| void transport.close(); | ||
| void server.close(); | ||
| }); | ||
| } | ||
| const body = await readJsonBody(req, maxBodyBytes); | ||
| await server.connect(transport); | ||
| res.on("close", () => { | ||
| void transport.close(); | ||
| void server.close(); | ||
| }); | ||
| await transport.handleRequest(req, res, body); | ||
| } | ||
| var port = readPort(); | ||
| var path = readPath(); | ||
| var httpServer = createServer(async (req, res) => { | ||
| const url = new URL( | ||
| req.url ?? "/", | ||
| `http://${req.headers.host ?? "localhost"}` | ||
| ); | ||
| if (url.pathname === "/healthz") { | ||
| sendJson(res, 200, { ok: true }); | ||
| return; | ||
| } | ||
| if (url.pathname !== path) { | ||
| sendJson(res, 404, jsonRpcError("Not found")); | ||
| return; | ||
| } | ||
| if (req.method !== "POST") { | ||
| sendJson(res, 405, jsonRpcError("Method not allowed")); | ||
| return; | ||
| } | ||
| try { | ||
| await handleMcpRequest(req, res); | ||
| } catch (error) { | ||
| console.error("MCP HTTP request failed", error); | ||
| if (!res.headersSent) { | ||
| sendJson(res, 500, jsonRpcError("Internal server error")); | ||
| function createMcpHttpServer(config2, requestHandler = handleMcpRequest) { | ||
| return createServer(async (req, res) => { | ||
| const url = new URL( | ||
| req.url ?? "/", | ||
| `http://${req.headers.host ?? "localhost"}` | ||
| ); | ||
| if (url.pathname === "/healthz") { | ||
| sendJson(res, 200, { ok: true }); | ||
| return; | ||
| } | ||
| } | ||
| }); | ||
| httpServer.listen(port, () => { | ||
| if (url.pathname !== config2.path) { | ||
| sendJson(res, 404, jsonRpcError("Not found")); | ||
| return; | ||
| } | ||
| if (req.method !== "POST") { | ||
| sendJson(res, 405, jsonRpcError("Method not allowed")); | ||
| return; | ||
| } | ||
| const origin = req.headers.origin; | ||
| if (origin && !config2.allowedOrigins.has(origin)) { | ||
| sendJson(res, 403, jsonRpcError("Origin not allowed")); | ||
| return; | ||
| } | ||
| if (!authorized(req, config2.bearerToken)) { | ||
| sendJson(res, 401, jsonRpcError("Unauthorized"), { | ||
| "www-authenticate": "Bearer" | ||
| }); | ||
| return; | ||
| } | ||
| try { | ||
| await requestHandler(req, res, config2.maxBodyBytes); | ||
| } catch (error) { | ||
| if (error instanceof BodyTooLargeError) { | ||
| sendJson(res, 413, jsonRpcError("Request body too large")); | ||
| return; | ||
| } | ||
| if (error instanceof InvalidJsonError) { | ||
| sendJson(res, 400, jsonRpcError("Invalid JSON request body")); | ||
| return; | ||
| } | ||
| console.error("MCP HTTP request failed", error); | ||
| if (!res.headersSent) { | ||
| sendJson(res, 500, jsonRpcError("Internal server error")); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| // src/http.ts | ||
| var config = readHttpConfig(); | ||
| var httpServer = createMcpHttpServer(config); | ||
| httpServer.listen(config.port, config.host, () => { | ||
| console.error( | ||
| `TypesenseKit MCP HTTP server listening on ${path} port ${port}` | ||
| `TypesenseKit MCP HTTP server listening on http://${config.host}:${config.port}${config.path}` | ||
| ); | ||
@@ -111,0 +201,0 @@ }); |
+29
-0
| import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; | ||
| type McpAuditEvent = { | ||
| timestamp: string; | ||
| operation: string; | ||
| outcome: "started" | "succeeded" | "failed"; | ||
| durationMs?: number; | ||
| errorName?: string; | ||
| }; | ||
| type McpAuditLogger = { | ||
| record: (event: McpAuditEvent) => void; | ||
| }; | ||
| type McpExecutionConfig = { | ||
| timeoutMs: number; | ||
| maxConcurrency: number; | ||
| rateLimitPerMinute: number; | ||
| maxResponseBytes: number; | ||
| }; | ||
| declare class McpExecutionController { | ||
| readonly config: McpExecutionConfig; | ||
| private active; | ||
| private starts; | ||
| private readonly clock; | ||
| constructor(config: McpExecutionConfig, clock?: () => number); | ||
| run<T>(task: () => Promise<T>): Promise<T>; | ||
| serialize(value: unknown): string; | ||
| } | ||
| type TypesenseMcpServerOptions = { | ||
| readOnly?: boolean; | ||
| executionController?: McpExecutionController; | ||
| auditLogger?: McpAuditLogger; | ||
| }; | ||
@@ -6,0 +35,0 @@ declare function createTypesenseMcpServer(options?: TypesenseMcpServerOptions): McpServer; |
+178
-6
@@ -1556,6 +1556,9 @@ // src/server.ts | ||
| // src/server.ts | ||
| import { z as z19 } from "zod"; | ||
| // package.json | ||
| var package_default = { | ||
| name: "@typesensekit/mcp", | ||
| version: "1.2.0", | ||
| version: "1.3.0", | ||
| type: "module", | ||
@@ -1582,3 +1585,4 @@ main: "dist/server.js", | ||
| typecheck: "tsc --noEmit", | ||
| test: "vitest run" | ||
| test: "vitest run", | ||
| "smoke:stdio": "node scripts/smoke-stdio.mjs" | ||
| }, | ||
@@ -1619,2 +1623,16 @@ dependencies: { | ||
| // src/audit.ts | ||
| var disabledLogger = { record: () => void 0 }; | ||
| function createMcpAuditLogger(env = process.env, sink = console.error) { | ||
| const enabled = ["1", "true", "yes", "on"].includes( | ||
| (env.TYPESENSEKIT_MCP_AUDIT_LOG ?? "").toLowerCase() | ||
| ); | ||
| if (!enabled) return disabledLogger; | ||
| return { | ||
| record(event) { | ||
| sink(JSON.stringify({ type: "typesensekit.mcp.tool", ...event })); | ||
| } | ||
| }; | ||
| } | ||
| // src/read-only.ts | ||
@@ -1693,2 +1711,104 @@ var READ_ONLY_OPERATION_NAMES = /* @__PURE__ */ new Set([ | ||
| // src/execution.ts | ||
| var DEFAULT_CONFIG = { | ||
| timeoutMs: 3e4, | ||
| maxConcurrency: 8, | ||
| rateLimitPerMinute: 120, | ||
| maxResponseBytes: 1024 * 1024 | ||
| }; | ||
| var McpExecutionLimitError = class extends Error { | ||
| }; | ||
| function positiveInteger(value, fallback, name) { | ||
| if (!value) return fallback; | ||
| const parsed = Number(value); | ||
| if (!Number.isInteger(parsed) || parsed <= 0) { | ||
| throw new Error(`Invalid ${name}: ${value}`); | ||
| } | ||
| return parsed; | ||
| } | ||
| function readMcpExecutionConfig(env = process.env) { | ||
| return { | ||
| timeoutMs: positiveInteger( | ||
| env.TYPESENSEKIT_MCP_TOOL_TIMEOUT_MS, | ||
| DEFAULT_CONFIG.timeoutMs, | ||
| "MCP tool timeout" | ||
| ), | ||
| maxConcurrency: positiveInteger( | ||
| env.TYPESENSEKIT_MCP_MAX_CONCURRENCY, | ||
| DEFAULT_CONFIG.maxConcurrency, | ||
| "MCP concurrency limit" | ||
| ), | ||
| rateLimitPerMinute: positiveInteger( | ||
| env.TYPESENSEKIT_MCP_RATE_LIMIT_PER_MINUTE, | ||
| DEFAULT_CONFIG.rateLimitPerMinute, | ||
| "MCP rate limit" | ||
| ), | ||
| maxResponseBytes: positiveInteger( | ||
| env.TYPESENSEKIT_MCP_MAX_RESPONSE_BYTES, | ||
| DEFAULT_CONFIG.maxResponseBytes, | ||
| "MCP response limit" | ||
| ) | ||
| }; | ||
| } | ||
| var McpExecutionController = class { | ||
| config; | ||
| active = 0; | ||
| starts = []; | ||
| clock; | ||
| constructor(config, clock = Date.now) { | ||
| this.config = config; | ||
| this.clock = clock; | ||
| } | ||
| async run(task) { | ||
| const now = this.clock(); | ||
| this.starts = this.starts.filter((started) => now - started < 6e4); | ||
| if (this.starts.length >= this.config.rateLimitPerMinute) { | ||
| throw new McpExecutionLimitError( | ||
| `MCP tool rate limit exceeded (${this.config.rateLimitPerMinute} calls per minute).` | ||
| ); | ||
| } | ||
| if (this.active >= this.config.maxConcurrency) { | ||
| throw new McpExecutionLimitError( | ||
| `MCP tool concurrency limit exceeded (${this.config.maxConcurrency} active calls).` | ||
| ); | ||
| } | ||
| this.starts.push(now); | ||
| this.active += 1; | ||
| let timeout; | ||
| try { | ||
| return await Promise.race([ | ||
| task(), | ||
| new Promise((_resolve, reject) => { | ||
| timeout = setTimeout( | ||
| () => reject( | ||
| new McpExecutionLimitError( | ||
| `MCP tool timed out after ${this.config.timeoutMs}ms.` | ||
| ) | ||
| ), | ||
| this.config.timeoutMs | ||
| ); | ||
| }) | ||
| ]); | ||
| } finally { | ||
| if (timeout) clearTimeout(timeout); | ||
| this.active -= 1; | ||
| } | ||
| } | ||
| serialize(value) { | ||
| const text = JSON.stringify(value, null, 2); | ||
| const bytes = Buffer.byteLength(text); | ||
| if (bytes > this.config.maxResponseBytes) { | ||
| throw new McpExecutionLimitError( | ||
| `MCP tool response is ${bytes} bytes, exceeding the ${this.config.maxResponseBytes}-byte limit. Narrow the request or increase TYPESENSEKIT_MCP_MAX_RESPONSE_BYTES.` | ||
| ); | ||
| } | ||
| return text; | ||
| } | ||
| }; | ||
| var sharedController; | ||
| function sharedMcpExecutionController() { | ||
| sharedController ??= new McpExecutionController(readMcpExecutionConfig()); | ||
| return sharedController; | ||
| } | ||
| // src/resources.ts | ||
@@ -1704,3 +1824,3 @@ import { | ||
| mimeType: "application/json", | ||
| text: JSON.stringify(value, null, 2) | ||
| text: JSON.stringify(redactSecrets(value), null, 2) | ||
| } | ||
@@ -1813,2 +1933,27 @@ ] | ||
| // src/tool-metadata.ts | ||
| var DESTRUCTIVE_OPERATION_NAMES = /* @__PURE__ */ new Set([ | ||
| "api.call", | ||
| "collections.delete", | ||
| "collections.fields.drop", | ||
| "collections.fields.replace", | ||
| "documents.delete", | ||
| "keys.delete" | ||
| ]); | ||
| function isDestructive(name) { | ||
| return DESTRUCTIVE_OPERATION_NAMES.has(name) || name.endsWith(".delete"); | ||
| } | ||
| function isIdempotent(name, readOnly) { | ||
| return readOnly || name.endsWith(".upsert") || name.endsWith(".update") || name === "collections.wait" || name === "operations.slow_requests.configure"; | ||
| } | ||
| function operationToolAnnotations(operation) { | ||
| const readOnly = READ_ONLY_OPERATION_NAMES.has(operation.name); | ||
| return { | ||
| readOnlyHint: readOnly, | ||
| destructiveHint: isDestructive(operation.name), | ||
| idempotentHint: isIdempotent(operation.name, readOnly), | ||
| openWorldHint: true | ||
| }; | ||
| } | ||
| // src/server.ts | ||
@@ -1826,2 +1971,4 @@ function toToolShape(input) { | ||
| const mcpOptions = { ...readMcpOptions(), ...options }; | ||
| const execution = options.executionController ?? sharedMcpExecutionController(); | ||
| const audit = options.auditLogger ?? createMcpAuditLogger(); | ||
| const activeOperations = filterMcpOperations(operations, mcpOptions.readOnly); | ||
@@ -1840,13 +1987,31 @@ registerTypesenseResources( | ||
| description: operation.summary, | ||
| inputSchema: toToolShape(operation.input) | ||
| inputSchema: toToolShape(operation.input), | ||
| outputSchema: { result: z19.unknown() }, | ||
| annotations: operationToolAnnotations(operation) | ||
| }, | ||
| async (args) => { | ||
| const startedAt = Date.now(); | ||
| audit.record({ | ||
| timestamp: new Date(startedAt).toISOString(), | ||
| operation: operation.name, | ||
| outcome: "started" | ||
| }); | ||
| try { | ||
| const input = operation.input.parse(args); | ||
| const result = await operation.execute(client, input); | ||
| const result = await execution.run( | ||
| () => operation.execute(client, input) | ||
| ); | ||
| const safeResult = redactSecrets(result); | ||
| audit.record({ | ||
| timestamp: (/* @__PURE__ */ new Date()).toISOString(), | ||
| operation: operation.name, | ||
| outcome: "succeeded", | ||
| durationMs: Date.now() - startedAt | ||
| }); | ||
| return { | ||
| structuredContent: { result: safeResult }, | ||
| content: [ | ||
| { | ||
| type: "text", | ||
| text: JSON.stringify(redactSecrets(result), null, 2) | ||
| text: execution.serialize(safeResult) | ||
| } | ||
@@ -1856,2 +2021,9 @@ ] | ||
| } catch (error) { | ||
| audit.record({ | ||
| timestamp: (/* @__PURE__ */ new Date()).toISOString(), | ||
| operation: operation.name, | ||
| outcome: "failed", | ||
| durationMs: Date.now() - startedAt, | ||
| errorName: error instanceof Error ? error.name : "UnknownError" | ||
| }); | ||
| const message = formatTypesenseErrorMessage(error); | ||
@@ -1858,0 +2030,0 @@ return { isError: true, content: [{ type: "text", text: message }] }; |
+3
-2
| { | ||
| "name": "@typesensekit/mcp", | ||
| "version": "1.2.0", | ||
| "version": "1.3.0", | ||
| "type": "module", | ||
@@ -57,4 +57,5 @@ "main": "dist/server.js", | ||
| "typecheck": "tsc --noEmit", | ||
| "test": "vitest run" | ||
| "test": "vitest run", | ||
| "smoke:stdio": "node scripts/smoke-stdio.mjs" | ||
| } | ||
| } |
| #!/usr/bin/env node | ||
| // src/server.ts | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| // ../core/src/client.ts | ||
| import Typesense from "typesense"; | ||
| // ../core/src/config.ts | ||
| import { z } from "zod"; | ||
| var nodeConfigSchema = z.object({ | ||
| host: z.string().min(1), | ||
| port: z.number().int().positive().optional(), | ||
| protocol: z.enum(["http", "https"]).optional(), | ||
| path: z.string().optional() | ||
| }); | ||
| var serverConfigSchema = z.object({ | ||
| url: z.string().url(), | ||
| apiKey: z.string().min(1), | ||
| connectionTimeoutSeconds: z.number().positive().optional(), | ||
| nearestNode: nodeConfigSchema.optional(), | ||
| numRetries: z.number().int().nonnegative().optional(), | ||
| retryIntervalSeconds: z.number().positive().optional(), | ||
| healthcheckIntervalSeconds: z.number().positive().optional() | ||
| }); | ||
| // ../core/src/client.ts | ||
| function nodeFromUrl(url) { | ||
| const parsed = new URL(url); | ||
| const node = { | ||
| host: parsed.hostname, | ||
| port: parsed.port ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80, | ||
| protocol: parsed.protocol.replace(":", "") | ||
| }; | ||
| if (parsed.pathname !== "/") node.path = parsed.pathname; | ||
| return node; | ||
| } | ||
| function normalizeNearestNode(node) { | ||
| if (!node) return void 0; | ||
| return { | ||
| host: node.host, | ||
| port: node.port ?? (node.protocol === "http" ? 80 : 443), | ||
| protocol: node.protocol ?? "https", | ||
| path: node.path | ||
| }; | ||
| } | ||
| function createClient(config) { | ||
| const parsed = serverConfigSchema.parse(config); | ||
| const clientConfig = { | ||
| nodes: [nodeFromUrl(parsed.url)], | ||
| apiKey: parsed.apiKey, | ||
| connectionTimeoutSeconds: parsed.connectionTimeoutSeconds, | ||
| nearestNode: normalizeNearestNode(parsed.nearestNode), | ||
| numRetries: parsed.numRetries, | ||
| retryIntervalSeconds: parsed.retryIntervalSeconds, | ||
| healthcheckIntervalSeconds: parsed.healthcheckIntervalSeconds | ||
| }; | ||
| return new Typesense.Client(clientConfig); | ||
| } | ||
| // ../core/src/redaction.ts | ||
| var REDACTED = "[REDACTED]"; | ||
| var CIRCULAR = "[Circular]"; | ||
| var SECRET_KEYS = /* @__PURE__ */ new Set([ | ||
| "api_key", | ||
| "apikey", | ||
| "authorization", | ||
| "cookie", | ||
| "secret", | ||
| "setcookie", | ||
| "token", | ||
| "xtypesenseapikey" | ||
| ]); | ||
| function shouldRedactKey(key) { | ||
| const normalized = key.toLowerCase().replace(/[-_\s]/g, ""); | ||
| return SECRET_KEYS.has(normalized) || normalized.endsWith("apikey") || normalized.endsWith("token") || normalized.endsWith("secret"); | ||
| } | ||
| function isTypesenseApiKeyShape(value) { | ||
| return Array.isArray(value.actions) && Array.isArray(value.collections); | ||
| } | ||
| function redactText(value) { | ||
| return value.replace( | ||
| /(["']?authorization["']?\s*[:=]\s*)(["']?)(?:Bearer|Basic)?\s*[-A-Za-z0-9._~+/=]+(["']?)/gi, | ||
| (_match, prefix, openQuote) => `${prefix}${openQuote}${REDACTED}${openQuote}` | ||
| ).replace( | ||
| /(["']?(?:x-typesense-api-key|api[_-]?key|apikey|token|secret|cookie|set-cookie)["']?\s*[:=]\s*)(["']?)([^"',\s}\]]+)(["']?)/gi, | ||
| (_match, prefix, openQuote, _secret) => `${prefix}${openQuote}${REDACTED}${openQuote}` | ||
| ).replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, `$1 ${REDACTED}`); | ||
| } | ||
| function redactError(error, seen) { | ||
| const output = {}; | ||
| for (const key of Object.getOwnPropertyNames(error)) { | ||
| output[key] = redactValue( | ||
| error[key], | ||
| seen | ||
| ); | ||
| } | ||
| if (!("name" in output)) output.name = error.name; | ||
| if (!("message" in output)) output.message = redactText(error.message); | ||
| return output; | ||
| } | ||
| function redactValue(value, seen) { | ||
| if (typeof value === "string") return redactText(value); | ||
| if (Array.isArray(value)) { | ||
| return value.map((item) => redactValue(item, seen)); | ||
| } | ||
| if (typeof value !== "object" || value === null) { | ||
| return value; | ||
| } | ||
| if (seen.has(value)) return CIRCULAR; | ||
| seen.add(value); | ||
| if (value instanceof Error) { | ||
| return redactError(value, seen); | ||
| } | ||
| const record = value; | ||
| return Object.fromEntries( | ||
| Object.entries(record).map(([key, child]) => [ | ||
| key, | ||
| shouldRedactKey(key) || key === "value" && isTypesenseApiKeyShape(record) ? REDACTED : redactValue(child, seen) | ||
| ]) | ||
| ); | ||
| } | ||
| function redactSecrets(value) { | ||
| return redactValue(value, /* @__PURE__ */ new WeakSet()); | ||
| } | ||
| // ../core/src/errors.ts | ||
| var NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([ | ||
| "EAI_AGAIN", | ||
| "ECONNABORTED", | ||
| "ECONNREFUSED", | ||
| "ECONNRESET", | ||
| "ENOTFOUND", | ||
| "ETIMEDOUT" | ||
| ]); | ||
| function readErrorLike(error) { | ||
| return typeof error === "object" && error !== null ? error : {}; | ||
| } | ||
| function findNetworkErrorCode(error) { | ||
| const errorLike = readErrorLike(error); | ||
| if (typeof errorLike.code === "string" && NETWORK_ERROR_CODES.has(errorLike.code.toUpperCase())) { | ||
| return errorLike.code.toUpperCase(); | ||
| } | ||
| if (typeof errorLike.message === "string") { | ||
| const match = errorLike.message.match( | ||
| /\b(EAI_AGAIN|ECONNABORTED|ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT)\b/i | ||
| ); | ||
| if (match?.[1]) return match[1].toUpperCase(); | ||
| } | ||
| return errorLike.cause === void 0 ? void 0 : findNetworkErrorCode(errorLike.cause); | ||
| } | ||
| function endpointLabel(error) { | ||
| const errorLike = readErrorLike(error); | ||
| const host = typeof errorLike.hostname === "string" ? errorLike.hostname : typeof errorLike.host === "string" ? errorLike.host : typeof errorLike.address === "string" ? errorLike.address : void 0; | ||
| const port = typeof errorLike.port === "number" || typeof errorLike.port === "string" ? String(errorLike.port) : void 0; | ||
| if (host && port) return `${host}:${port}`; | ||
| if (host) return host; | ||
| return errorLike.cause === void 0 ? void 0 : endpointLabel(errorLike.cause); | ||
| } | ||
| function conciseNetworkErrorMessage(error) { | ||
| const code = findNetworkErrorCode(error); | ||
| if (!code) return void 0; | ||
| const endpoint = endpointLabel(error); | ||
| return `Request failed: ${code}${endpoint ? ` ${endpoint}` : ""}`; | ||
| } | ||
| function normalizeTypesenseError(error) { | ||
| if (error instanceof Error) { | ||
| const errorLike = error; | ||
| const status = errorLike.httpStatus ?? errorLike.status; | ||
| return { | ||
| code: typeof status === "number" ? String(status) : error.name || "TypesenseError", | ||
| message: redactText(error.message), | ||
| details: redactSecrets(error) | ||
| }; | ||
| } | ||
| if (typeof error === "object" && error !== null) { | ||
| const errorLike = error; | ||
| return { | ||
| code: typeof errorLike.status === "number" ? String(errorLike.status) : "TypesenseError", | ||
| message: typeof errorLike.message === "string" ? redactText(errorLike.message) : "Unknown Typesense error", | ||
| details: redactSecrets(error) | ||
| }; | ||
| } | ||
| return { code: "TypesenseError", message: redactText(String(error)) }; | ||
| } | ||
| function formatTypesenseErrorMessage(error, options = {}) { | ||
| const normalized = normalizeTypesenseError(error); | ||
| const message = conciseNetworkErrorMessage(error) ?? normalized.message; | ||
| if (!options.debug) return message; | ||
| return [ | ||
| message, | ||
| "", | ||
| "Debug details:", | ||
| JSON.stringify(normalized.details ?? normalized, null, 2) | ||
| ].join("\n"); | ||
| } | ||
| // ../core/src/operations/aliases.ts | ||
| import { z as z2 } from "zod"; | ||
| // ../core/src/operations/http.ts | ||
| function api(client) { | ||
| return client.apiCall; | ||
| } | ||
| function enc(value) { | ||
| return encodeURIComponent(value); | ||
| } | ||
| function collectionPath(collection) { | ||
| return `/collections/${enc(collection)}`; | ||
| } | ||
| // ../core/src/operations/aliases.ts | ||
| var aliasesOperations = [ | ||
| { | ||
| name: "aliases.list", | ||
| summary: "List aliases", | ||
| category: "aliases", | ||
| input: z2.object({}), | ||
| execute: async (client) => api(client).get("/aliases") | ||
| }, | ||
| { | ||
| name: "aliases.create", | ||
| summary: "Create or upsert an alias", | ||
| category: "aliases", | ||
| input: z2.object({ | ||
| name: z2.string(), | ||
| value: z2.object({ collection_name: z2.string() }) | ||
| }), | ||
| execute: async (client, input) => api(client).put(`/aliases/${enc(input.name)}`, input.value) | ||
| }, | ||
| { | ||
| name: "aliases.retrieve", | ||
| summary: "Retrieve an alias", | ||
| category: "aliases", | ||
| input: z2.object({ name: z2.string() }), | ||
| execute: async (client, input) => api(client).get(`/aliases/${enc(input.name)}`) | ||
| }, | ||
| { | ||
| name: "aliases.delete", | ||
| summary: "Delete an alias", | ||
| category: "aliases", | ||
| input: z2.object({ name: z2.string() }), | ||
| execute: async (client, input) => api(client).delete(`/aliases/${enc(input.name)}`) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/analytics.ts | ||
| import { z as z3 } from "zod"; | ||
| var analyticsOperations = [ | ||
| { | ||
| name: "analytics.rules.list", | ||
| summary: "List analytics rules", | ||
| category: "analytics", | ||
| input: z3.object({ ruleTag: z3.string().optional() }), | ||
| execute: async (client, input) => api(client).get( | ||
| "/analytics/rules", | ||
| input.ruleTag ? { rule_tag: input.ruleTag } : void 0 | ||
| ) | ||
| }, | ||
| { | ||
| name: "analytics.rules.create", | ||
| summary: "Create one or more analytics rules", | ||
| category: "analytics", | ||
| input: z3.object({ | ||
| value: z3.union([ | ||
| z3.record(z3.unknown()), | ||
| z3.array(z3.record(z3.unknown())).min(1) | ||
| ]) | ||
| }), | ||
| execute: async (client, input) => api(client).post("/analytics/rules", input.value) | ||
| }, | ||
| { | ||
| name: "analytics.rules.upsert", | ||
| summary: "Create or update an analytics rule", | ||
| category: "analytics", | ||
| input: z3.object({ name: z3.string(), value: z3.record(z3.unknown()) }), | ||
| execute: async (client, input) => api(client).put(`/analytics/rules/${enc(input.name)}`, input.value) | ||
| }, | ||
| { | ||
| name: "analytics.rules.delete", | ||
| summary: "Delete an analytics rule", | ||
| category: "analytics", | ||
| input: z3.object({ name: z3.string() }), | ||
| execute: async (client, input) => api(client).delete(`/analytics/rules/${enc(input.name)}`) | ||
| }, | ||
| { | ||
| name: "analytics.rules.retrieve", | ||
| summary: "Retrieve an analytics rule", | ||
| category: "analytics", | ||
| input: z3.object({ name: z3.string() }), | ||
| execute: async (client, input) => api(client).get(`/analytics/rules/${enc(input.name)}`) | ||
| }, | ||
| { | ||
| name: "analytics.events.create", | ||
| summary: "Create an analytics event", | ||
| category: "analytics", | ||
| input: z3.object({ | ||
| type: z3.string(), | ||
| name: z3.string(), | ||
| data: z3.record(z3.unknown()) | ||
| }), | ||
| execute: async (client, input) => api(client).post("/analytics/events", input) | ||
| }, | ||
| { | ||
| name: "analytics.events.list", | ||
| summary: "Retrieve recent analytics events for a user and rule", | ||
| category: "analytics", | ||
| input: z3.object({ | ||
| userId: z3.string().min(1), | ||
| name: z3.string().min(1), | ||
| limit: z3.number().int().positive().max(1e3) | ||
| }), | ||
| execute: async (client, input) => api(client).get("/analytics/events", { | ||
| user_id: input.userId, | ||
| name: input.name, | ||
| n: input.limit | ||
| }) | ||
| }, | ||
| { | ||
| name: "analytics.flush", | ||
| summary: "Flush in-memory analytics data to persistent storage", | ||
| category: "analytics", | ||
| input: z3.object({}), | ||
| execute: async (client) => api(client).post("/analytics/flush") | ||
| }, | ||
| { | ||
| name: "analytics.status", | ||
| summary: "Retrieve analytics subsystem status", | ||
| category: "analytics", | ||
| input: z3.object({}), | ||
| execute: async (client) => api(client).get("/analytics/status") | ||
| } | ||
| ]; | ||
| // ../core/src/operations/api.ts | ||
| import { z as z4 } from "zod"; | ||
| var methodSchema = z4.enum(["get", "post", "put", "patch", "delete"]); | ||
| var normalizedMethodSchema = z4.preprocess( | ||
| (value) => typeof value === "string" ? value.toLowerCase() : value, | ||
| methodSchema | ||
| ); | ||
| var apiOperations = [ | ||
| { | ||
| name: "api.call", | ||
| summary: "Call any Typesense API endpoint not yet covered by a first-class operation", | ||
| category: "api", | ||
| input: z4.object({ | ||
| method: normalizedMethodSchema, | ||
| path: z4.string().startsWith("/"), | ||
| params: z4.record(z4.unknown()).optional(), | ||
| body: z4.unknown().optional() | ||
| }), | ||
| execute: async (client, input) => { | ||
| const request = api(client); | ||
| if (input.method === "get") return request.get(input.path, input.params); | ||
| if (input.method === "delete") | ||
| return request.delete(input.path, input.params); | ||
| if (input.method === "post") | ||
| return request.post(input.path, input.body, input.params); | ||
| if (input.method === "put") | ||
| return request.put(input.path, input.body, input.params); | ||
| return request.patch(input.path, input.body, input.params); | ||
| } | ||
| } | ||
| ]; | ||
| // ../core/src/operations/collections.ts | ||
| import { z as z5 } from "zod"; | ||
| var DEFAULT_WAIT_TIMEOUT_MS = 3e4; | ||
| var DEFAULT_WAIT_INTERVAL_MS = 1e3; | ||
| var baseFieldSchema = z5.object({ | ||
| name: z5.string(), | ||
| facet: z5.boolean().optional(), | ||
| index: z5.boolean().optional(), | ||
| optional: z5.boolean().optional(), | ||
| sort: z5.boolean().optional(), | ||
| locale: z5.string().optional(), | ||
| infix: z5.boolean().optional(), | ||
| stem: z5.boolean().optional() | ||
| }).passthrough(); | ||
| var createFieldSchema = baseFieldSchema.extend({ | ||
| type: z5.string() | ||
| }); | ||
| var patchFieldSchema = baseFieldSchema.extend({ | ||
| type: z5.string().optional(), | ||
| drop: z5.boolean().optional() | ||
| }); | ||
| var fieldLifecycleInputSchema = z5.object({ | ||
| collection: z5.string(), | ||
| field: z5.string().optional(), | ||
| yes: z5.boolean().optional(), | ||
| numDim: z5.coerce.number().int().positive().optional(), | ||
| vecDist: z5.string().optional(), | ||
| hnswM: z5.coerce.number().int().positive().optional(), | ||
| hnswEfConstruction: z5.coerce.number().int().positive().optional(), | ||
| embedFrom: z5.union([z5.string(), z5.array(z5.string())]).optional(), | ||
| embedModel: z5.string().optional(), | ||
| embedApiKey: z5.string().optional(), | ||
| timeoutMs: z5.coerce.number().int().nonnegative().default(DEFAULT_WAIT_TIMEOUT_MS), | ||
| intervalMs: z5.coerce.number().int().nonnegative().default(DEFAULT_WAIT_INTERVAL_MS) | ||
| }).passthrough(); | ||
| var waitInputSchema = z5.object({ | ||
| collection: z5.string(), | ||
| fieldPresent: z5.string().optional(), | ||
| fieldMissing: z5.string().optional(), | ||
| fieldEmbedFrom: z5.string().optional(), | ||
| timeoutMs: z5.coerce.number().int().nonnegative().default(DEFAULT_WAIT_TIMEOUT_MS), | ||
| intervalMs: z5.coerce.number().int().nonnegative().default(DEFAULT_WAIT_INTERVAL_MS) | ||
| }); | ||
| var FIELD_LIFECYCLE_KEYS = /* @__PURE__ */ new Set([ | ||
| "collection", | ||
| "field", | ||
| "yes", | ||
| "numDim", | ||
| "vecDist", | ||
| "hnswM", | ||
| "hnswEfConstruction", | ||
| "embedFrom", | ||
| "embedModel", | ||
| "embedApiKey", | ||
| "timeoutMs", | ||
| "intervalMs" | ||
| ]); | ||
| function sleep(ms) { | ||
| return new Promise((resolve) => setTimeout(resolve, ms)); | ||
| } | ||
| function isObject(value) { | ||
| return typeof value === "object" && value !== null && !Array.isArray(value); | ||
| } | ||
| function requestedFieldName(input) { | ||
| const name = input.field ?? input.name; | ||
| if (typeof name !== "string" || !name) { | ||
| throw new Error("A field name is required. Pass --field or include name."); | ||
| } | ||
| return name; | ||
| } | ||
| function requiresConfirmation(collection) { | ||
| return /^production(?:__|-|$)/i.test(collection) || /^prod(?:__|-|$)/i.test(collection); | ||
| } | ||
| function assertDestructiveConfirmed(input) { | ||
| if (requiresConfirmation(input.collection) && !input.yes) { | ||
| throw new Error( | ||
| `Refusing to modify production collection ${input.collection} without --yes.` | ||
| ); | ||
| } | ||
| } | ||
| function findField(collection, fieldName) { | ||
| return collection.fields?.find((field) => { | ||
| if (!isObject(field)) return false; | ||
| return field.name === fieldName; | ||
| }); | ||
| } | ||
| async function retrieveCollection(client, collection) { | ||
| return api(client).get(collectionPath(collection)); | ||
| } | ||
| function buildFieldDefinition(input) { | ||
| const field = Object.fromEntries( | ||
| Object.entries(input).filter( | ||
| ([key, value]) => value !== void 0 && !FIELD_LIFECYCLE_KEYS.has(key) | ||
| ) | ||
| ); | ||
| field.name = requestedFieldName(input); | ||
| if (input.numDim !== void 0) field.num_dim = input.numDim; | ||
| if (input.vecDist !== void 0) field.vec_dist = input.vecDist; | ||
| if (input.hnswM !== void 0 || input.hnswEfConstruction !== void 0) { | ||
| const hnswParams = isObject(field.hnsw_params) ? field.hnsw_params : {}; | ||
| if (input.hnswM !== void 0) hnswParams.M = input.hnswM; | ||
| if (input.hnswEfConstruction !== void 0) { | ||
| hnswParams.ef_construction = input.hnswEfConstruction; | ||
| } | ||
| field.hnsw_params = hnswParams; | ||
| } | ||
| if (input.embedFrom !== void 0 || input.embedModel !== void 0 || input.embedApiKey !== void 0) { | ||
| const embed = isObject(field.embed) ? field.embed : {}; | ||
| const modelConfig = isObject(embed.model_config) ? embed.model_config : {}; | ||
| if (input.embedFrom !== void 0) { | ||
| embed.from = Array.isArray(input.embedFrom) ? input.embedFrom : [input.embedFrom]; | ||
| } | ||
| if (input.embedModel !== void 0) | ||
| modelConfig.model_name = input.embedModel; | ||
| if (input.embedApiKey !== void 0) | ||
| modelConfig.api_key = input.embedApiKey; | ||
| embed.model_config = modelConfig; | ||
| field.embed = embed; | ||
| } | ||
| if (typeof field.type !== "string" || !field.type) { | ||
| throw new Error("A field type is required. Pass --type or include type."); | ||
| } | ||
| return field; | ||
| } | ||
| function parseEmbedFromCondition(condition) { | ||
| const separator = condition.indexOf(":"); | ||
| if (separator === -1) { | ||
| throw new Error("--field-embed-from must use FIELD:SOURCE format."); | ||
| } | ||
| return { | ||
| fieldName: condition.slice(0, separator), | ||
| source: condition.slice(separator + 1) | ||
| }; | ||
| } | ||
| function isTransientCollectionUpdateError(error) { | ||
| const message = error instanceof Error ? error.message : String(error); | ||
| return /another collection update operation is in progress/i.test(message) || /timeout of \d+ms exceeded/i.test(message) || /econnaborted/i.test(message); | ||
| } | ||
| function evaluateWaitCondition(collection, input) { | ||
| const conditions = [ | ||
| input.fieldPresent, | ||
| input.fieldMissing, | ||
| input.fieldEmbedFrom | ||
| ].filter(Boolean); | ||
| if (conditions.length !== 1) { | ||
| throw new Error( | ||
| "Pass exactly one wait condition: --field-present, --field-missing, or --field-embed-from." | ||
| ); | ||
| } | ||
| if (input.fieldPresent) { | ||
| return { | ||
| ok: Boolean(findField(collection, input.fieldPresent)), | ||
| condition: "field-present", | ||
| field: input.fieldPresent | ||
| }; | ||
| } | ||
| if (input.fieldMissing) { | ||
| return { | ||
| ok: !findField(collection, input.fieldMissing), | ||
| condition: "field-missing", | ||
| field: input.fieldMissing | ||
| }; | ||
| } | ||
| const { fieldName, source } = parseEmbedFromCondition( | ||
| input.fieldEmbedFrom ?? "" | ||
| ); | ||
| const field = findField(collection, fieldName); | ||
| const from = isObject(field) && isObject(field.embed) && Array.isArray(field.embed.from) ? field.embed.from : []; | ||
| return { | ||
| ok: from.includes(source), | ||
| condition: "field-embed-from", | ||
| field: fieldName, | ||
| source | ||
| }; | ||
| } | ||
| async function waitForCollectionCondition(client, input) { | ||
| const startedAt = Date.now(); | ||
| let lastError; | ||
| let attempts = 0; | ||
| while (Date.now() - startedAt <= input.timeoutMs) { | ||
| attempts += 1; | ||
| try { | ||
| const collection = await retrieveCollection(client, input.collection); | ||
| const result = evaluateWaitCondition(collection, input); | ||
| if (result.ok) { | ||
| return { | ||
| collection: input.collection, | ||
| attempts, | ||
| ...result | ||
| }; | ||
| } | ||
| } catch (error) { | ||
| if (!isTransientCollectionUpdateError(error)) throw error; | ||
| lastError = error; | ||
| } | ||
| await sleep(input.intervalMs); | ||
| } | ||
| const detail = lastError instanceof Error ? ` Last error: ${lastError.message}` : ""; | ||
| throw new Error( | ||
| `Timed out waiting for collection ${input.collection} schema condition.${detail}` | ||
| ); | ||
| } | ||
| async function dropField(client, input) { | ||
| assertDestructiveConfirmed(input); | ||
| const fieldName = requestedFieldName(input); | ||
| const collection = await retrieveCollection(client, input.collection); | ||
| const existingField = findField(collection, fieldName); | ||
| if (!existingField) { | ||
| return { | ||
| ok: true, | ||
| collection: input.collection, | ||
| field: fieldName, | ||
| alreadyMissing: true | ||
| }; | ||
| } | ||
| const result = await api(client).patch(collectionPath(input.collection), { | ||
| fields: [{ name: fieldName, drop: true }] | ||
| }); | ||
| return { | ||
| ok: true, | ||
| collection: input.collection, | ||
| field: fieldName, | ||
| before: existingField, | ||
| result | ||
| }; | ||
| } | ||
| async function addField(client, input) { | ||
| const field = buildFieldDefinition(input); | ||
| const result = await api(client).patch(collectionPath(input.collection), { | ||
| fields: [field] | ||
| }); | ||
| return { | ||
| ok: true, | ||
| collection: input.collection, | ||
| field: field.name, | ||
| added: field, | ||
| result | ||
| }; | ||
| } | ||
| var collectionOperations = [ | ||
| { | ||
| name: "collections.create", | ||
| summary: "Create a collection", | ||
| category: "collections", | ||
| input: z5.object({ | ||
| name: z5.string(), | ||
| fields: z5.array(createFieldSchema), | ||
| default_sorting_field: z5.string().optional(), | ||
| token_separators: z5.array(z5.string()).optional(), | ||
| symbols_to_index: z5.array(z5.string()).optional(), | ||
| enable_nested_fields: z5.boolean().optional() | ||
| }), | ||
| execute: async (client, input) => api(client).post("/collections", input) | ||
| }, | ||
| { | ||
| name: "collections.list", | ||
| summary: "List collections", | ||
| category: "collections", | ||
| input: z5.object({}), | ||
| execute: async (client) => api(client).get("/collections") | ||
| }, | ||
| { | ||
| name: "collections.retrieve", | ||
| summary: "Retrieve a collection", | ||
| category: "collections", | ||
| input: z5.object({ collection: z5.string() }), | ||
| execute: async (client, input) => api(client).get(collectionPath(input.collection)) | ||
| }, | ||
| { | ||
| name: "collections.update", | ||
| summary: "Update a collection schema", | ||
| category: "collections", | ||
| input: z5.object({ | ||
| collection: z5.string(), | ||
| fields: z5.array(patchFieldSchema).optional() | ||
| }), | ||
| execute: async (client, input) => api(client).patch(collectionPath(input.collection), { | ||
| fields: input.fields | ||
| }) | ||
| }, | ||
| { | ||
| name: "collections.wait", | ||
| summary: "Wait for a collection schema condition", | ||
| category: "collections", | ||
| input: waitInputSchema, | ||
| execute: waitForCollectionCondition | ||
| }, | ||
| { | ||
| name: "collections.fields.add", | ||
| summary: "Add a field to a collection", | ||
| category: "collections", | ||
| input: fieldLifecycleInputSchema, | ||
| execute: addField | ||
| }, | ||
| { | ||
| name: "collections.fields.drop", | ||
| summary: "Drop a field from a collection", | ||
| category: "collections", | ||
| input: fieldLifecycleInputSchema, | ||
| execute: dropField | ||
| }, | ||
| { | ||
| name: "collections.fields.replace", | ||
| summary: "Safely replace a collection field", | ||
| category: "collections", | ||
| input: fieldLifecycleInputSchema, | ||
| execute: async (client, input) => { | ||
| const fieldName = requestedFieldName(input); | ||
| const replacement = buildFieldDefinition(input); | ||
| const dropResult = await dropField(client, input); | ||
| await waitForCollectionCondition(client, { | ||
| collection: input.collection, | ||
| fieldMissing: fieldName, | ||
| timeoutMs: input.timeoutMs, | ||
| intervalMs: input.intervalMs | ||
| }); | ||
| try { | ||
| await api(client).patch(collectionPath(input.collection), { | ||
| fields: [replacement] | ||
| }); | ||
| } catch (error) { | ||
| throw new Error( | ||
| [ | ||
| error instanceof Error ? error.message : String(error), | ||
| "", | ||
| "The original field was dropped, but adding the replacement failed.", | ||
| `Recover with: tsk collections.fields.add --collection ${input.collection} --input field.json` | ||
| ].join("\n") | ||
| ); | ||
| } | ||
| const waitResult = await waitForCollectionCondition(client, { | ||
| collection: input.collection, | ||
| fieldPresent: fieldName, | ||
| timeoutMs: input.timeoutMs, | ||
| intervalMs: input.intervalMs | ||
| }); | ||
| const collection = await retrieveCollection(client, input.collection); | ||
| return { | ||
| ok: true, | ||
| collection: input.collection, | ||
| field: fieldName, | ||
| before: "before" in dropResult ? dropResult.before : void 0, | ||
| after: findField(collection, fieldName), | ||
| wait: waitResult | ||
| }; | ||
| } | ||
| }, | ||
| { | ||
| name: "collections.delete", | ||
| summary: "Delete a collection", | ||
| category: "collections", | ||
| input: z5.object({ collection: z5.string() }), | ||
| execute: async (client, input) => api(client).delete(collectionPath(input.collection)) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/conversations.ts | ||
| import { z as z6 } from "zod"; | ||
| var conversationOperations = [ | ||
| { | ||
| name: "conversations.models.list", | ||
| summary: "List conversation models", | ||
| category: "conversations", | ||
| input: z6.object({}), | ||
| execute: async (client) => api(client).get("/conversations/models") | ||
| }, | ||
| { | ||
| name: "conversations.models.create", | ||
| summary: "Create a conversation model", | ||
| category: "conversations", | ||
| input: z6.object({ value: z6.record(z6.unknown()) }), | ||
| execute: async (client, input) => api(client).post("/conversations/models", input.value) | ||
| }, | ||
| { | ||
| name: "conversations.models.retrieve", | ||
| summary: "Retrieve a conversation model", | ||
| category: "conversations", | ||
| input: z6.object({ id: z6.string() }), | ||
| execute: async (client, input) => api(client).get(`/conversations/models/${enc(input.id)}`) | ||
| }, | ||
| { | ||
| name: "conversations.models.delete", | ||
| summary: "Delete a conversation model", | ||
| category: "conversations", | ||
| input: z6.object({ id: z6.string() }), | ||
| execute: async (client, input) => api(client).delete(`/conversations/models/${enc(input.id)}`) | ||
| }, | ||
| { | ||
| name: "conversations.history.retrieve", | ||
| summary: "Retrieve conversation history", | ||
| category: "conversations", | ||
| input: z6.object({ conversation_id: z6.string().optional() }), | ||
| execute: async (client, input) => api(client).get( | ||
| "/conversations/history", | ||
| input.conversation_id ? { conversation_id: input.conversation_id } : void 0 | ||
| ) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/curation-sets.ts | ||
| import { z as z7 } from "zod"; | ||
| function base(name) { | ||
| return name ? `/curation_sets/${enc(name)}` : "/curation_sets"; | ||
| } | ||
| function itemPath(name, id) { | ||
| const path = `${base(name)}/items`; | ||
| return id ? `${path}/${enc(id)}` : path; | ||
| } | ||
| var curationSetOperations = [ | ||
| { | ||
| name: "curation_sets.list", | ||
| summary: "List global curation sets", | ||
| category: "curations", | ||
| input: z7.object({}), | ||
| execute: async (client) => api(client).get(base()) | ||
| }, | ||
| { | ||
| name: "curation_sets.upsert", | ||
| summary: "Create or update a global curation set", | ||
| category: "curations", | ||
| input: z7.object({ | ||
| name: z7.string(), | ||
| value: z7.object({ items: z7.array(z7.record(z7.unknown())) }) | ||
| }), | ||
| execute: async (client, input) => api(client).put(base(input.name), input.value) | ||
| }, | ||
| { | ||
| name: "curation_sets.retrieve", | ||
| summary: "Retrieve a global curation set", | ||
| category: "curations", | ||
| input: z7.object({ name: z7.string() }), | ||
| execute: async (client, input) => api(client).get(base(input.name)) | ||
| }, | ||
| { | ||
| name: "curation_sets.delete", | ||
| summary: "Delete a global curation set", | ||
| category: "curations", | ||
| input: z7.object({ name: z7.string() }), | ||
| execute: async (client, input) => api(client).delete(base(input.name)) | ||
| }, | ||
| { | ||
| name: "curation_sets.items.list", | ||
| summary: "List items in a global curation set", | ||
| category: "curations", | ||
| input: z7.object({ name: z7.string() }), | ||
| execute: async (client, input) => api(client).get(itemPath(input.name)) | ||
| }, | ||
| { | ||
| name: "curation_sets.items.upsert", | ||
| summary: "Create or update an item in a global curation set", | ||
| category: "curations", | ||
| input: z7.object({ | ||
| name: z7.string(), | ||
| id: z7.string(), | ||
| value: z7.record(z7.unknown()) | ||
| }), | ||
| execute: async (client, input) => api(client).put(itemPath(input.name, input.id), input.value) | ||
| }, | ||
| { | ||
| name: "curation_sets.items.retrieve", | ||
| summary: "Retrieve an item in a global curation set", | ||
| category: "curations", | ||
| input: z7.object({ name: z7.string(), id: z7.string() }), | ||
| execute: async (client, input) => api(client).get(itemPath(input.name, input.id)) | ||
| }, | ||
| { | ||
| name: "curation_sets.items.delete", | ||
| summary: "Delete an item in a global curation set", | ||
| category: "curations", | ||
| input: z7.object({ name: z7.string(), id: z7.string() }), | ||
| execute: async (client, input) => api(client).delete(itemPath(input.name, input.id)) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/documents.ts | ||
| import { z as z8 } from "zod"; | ||
| var documentSchema = z8.record(z8.unknown()); | ||
| var idsSchema = z8.array(z8.string().min(1)).min(1); | ||
| var searchParams = z8.record( | ||
| z8.union([ | ||
| z8.string(), | ||
| z8.number(), | ||
| z8.boolean(), | ||
| z8.array(z8.string()), | ||
| z8.array(z8.number()) | ||
| ]) | ||
| ); | ||
| var documentOperations = [ | ||
| { | ||
| name: "documents.index", | ||
| summary: "Index a document", | ||
| category: "documents", | ||
| input: z8.object({ collection: z8.string(), document: documentSchema }), | ||
| execute: async (client, input) => api(client).post( | ||
| `${collectionPath(input.collection)}/documents`, | ||
| input.document | ||
| ) | ||
| }, | ||
| { | ||
| name: "documents.upsert", | ||
| summary: "Upsert a document", | ||
| category: "documents", | ||
| input: z8.object({ collection: z8.string(), document: documentSchema }), | ||
| execute: async (client, input) => api(client).post( | ||
| `${collectionPath(input.collection)}/documents`, | ||
| input.document, | ||
| { action: "upsert" } | ||
| ) | ||
| }, | ||
| { | ||
| name: "documents.get", | ||
| summary: "Get a document by id", | ||
| category: "documents", | ||
| input: z8.object({ collection: z8.string(), id: z8.string() }), | ||
| execute: async (client, input) => api(client).get( | ||
| `${collectionPath(input.collection)}/documents/${enc(input.id)}` | ||
| ) | ||
| }, | ||
| { | ||
| name: "documents.get_many", | ||
| summary: "Get multiple documents by id", | ||
| category: "documents", | ||
| input: z8.object({ collection: z8.string(), ids: idsSchema }), | ||
| execute: async (client, input) => { | ||
| const request = api(client); | ||
| return Promise.all( | ||
| input.ids.map( | ||
| (id) => request.get( | ||
| `${collectionPath(input.collection)}/documents/${enc(id)}` | ||
| ) | ||
| ) | ||
| ); | ||
| } | ||
| }, | ||
| { | ||
| name: "documents.update", | ||
| summary: "Update a document by id", | ||
| category: "documents", | ||
| input: z8.object({ | ||
| collection: z8.string(), | ||
| id: z8.string(), | ||
| document: documentSchema | ||
| }), | ||
| execute: async (client, input) => api(client).patch( | ||
| `${collectionPath(input.collection)}/documents/${enc(input.id)}`, | ||
| input.document | ||
| ) | ||
| }, | ||
| { | ||
| name: "documents.delete", | ||
| summary: "Delete a document by id", | ||
| category: "documents", | ||
| input: z8.object({ collection: z8.string(), id: z8.string() }), | ||
| execute: async (client, input) => api(client).delete( | ||
| `${collectionPath(input.collection)}/documents/${enc(input.id)}` | ||
| ) | ||
| }, | ||
| { | ||
| name: "documents.import", | ||
| summary: "Import documents into a collection", | ||
| category: "documents", | ||
| input: z8.object({ | ||
| collection: z8.string(), | ||
| documents: z8.union([z8.string(), z8.array(documentSchema)]), | ||
| action: z8.enum(["create", "upsert", "update", "emplace"]).optional() | ||
| }), | ||
| execute: async (client, input) => api(client).post( | ||
| `${collectionPath(input.collection)}/documents/import`, | ||
| Array.isArray(input.documents) ? input.documents.map((doc) => JSON.stringify(doc)).join("\n") : input.documents, | ||
| input.action ? { action: input.action } : void 0 | ||
| ) | ||
| }, | ||
| { | ||
| name: "documents.export", | ||
| summary: "Export documents from a collection", | ||
| category: "documents", | ||
| input: z8.object({ | ||
| collection: z8.string(), | ||
| params: searchParams.optional() | ||
| }), | ||
| execute: async (client, input) => api(client).get( | ||
| `${collectionPath(input.collection)}/documents/export`, | ||
| input.params | ||
| ) | ||
| }, | ||
| { | ||
| name: "documents.search", | ||
| summary: "Search within a collection", | ||
| category: "documents", | ||
| input: z8.object({ collection: z8.string(), params: searchParams }), | ||
| execute: async (client, input) => api(client).get( | ||
| `${collectionPath(input.collection)}/documents/search`, | ||
| input.params | ||
| ) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/keys.ts | ||
| import { z as z9 } from "zod"; | ||
| var keysOperations = [ | ||
| { | ||
| name: "keys.list", | ||
| summary: "List API keys", | ||
| category: "keys", | ||
| input: z9.object({}), | ||
| execute: async (client) => api(client).get("/keys") | ||
| }, | ||
| { | ||
| name: "keys.create", | ||
| summary: "Create an API key", | ||
| category: "keys", | ||
| input: z9.object({ value: z9.record(z9.unknown()) }), | ||
| execute: async (client, input) => api(client).post("/keys", input.value) | ||
| }, | ||
| { | ||
| name: "keys.retrieve", | ||
| summary: "Retrieve an API key", | ||
| category: "keys", | ||
| input: z9.object({ id: z9.union([z9.string(), z9.number()]) }), | ||
| execute: async (client, input) => api(client).get(`/keys/${enc(String(input.id))}`) | ||
| }, | ||
| { | ||
| name: "keys.delete", | ||
| summary: "Delete an API key", | ||
| category: "keys", | ||
| input: z9.object({ id: z9.union([z9.string(), z9.number()]) }), | ||
| execute: async (client, input) => api(client).delete(`/keys/${enc(String(input.id))}`) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/nl-search-models.ts | ||
| import { z as z10 } from "zod"; | ||
| var modelConfigSchema = z10.object({ | ||
| id: z10.string().optional(), | ||
| model_name: z10.string().optional(), | ||
| api_key: z10.string().optional(), | ||
| api_url: z10.string().url().optional(), | ||
| max_bytes: z10.number().int().positive().optional(), | ||
| temperature: z10.number().optional(), | ||
| system_prompt: z10.string().optional(), | ||
| top_p: z10.number().optional(), | ||
| top_k: z10.number().int().optional(), | ||
| stop_sequences: z10.array(z10.string()).optional(), | ||
| api_version: z10.string().optional(), | ||
| project_id: z10.string().optional(), | ||
| access_token: z10.string().optional(), | ||
| refresh_token: z10.string().optional(), | ||
| client_id: z10.string().optional(), | ||
| client_secret: z10.string().optional(), | ||
| region: z10.string().optional(), | ||
| max_output_tokens: z10.number().int().positive().optional(), | ||
| account_id: z10.string().optional() | ||
| }).passthrough(); | ||
| function modelPath(id) { | ||
| return `/nl_search_models/${enc(id)}`; | ||
| } | ||
| var nlSearchModelOperations = [ | ||
| { | ||
| name: "nl_search_models.list", | ||
| summary: "List natural language search models", | ||
| category: "nl_search_models", | ||
| input: z10.object({}), | ||
| execute: async (client) => api(client).get("/nl_search_models") | ||
| }, | ||
| { | ||
| name: "nl_search_models.create", | ||
| summary: "Create a natural language search model", | ||
| category: "nl_search_models", | ||
| input: z10.object({ value: modelConfigSchema }), | ||
| execute: async (client, input) => api(client).post("/nl_search_models", input.value) | ||
| }, | ||
| { | ||
| name: "nl_search_models.retrieve", | ||
| summary: "Retrieve a natural language search model", | ||
| category: "nl_search_models", | ||
| input: z10.object({ id: z10.string().min(1) }), | ||
| execute: async (client, input) => api(client).get(modelPath(input.id)) | ||
| }, | ||
| { | ||
| name: "nl_search_models.update", | ||
| summary: "Update a natural language search model", | ||
| category: "nl_search_models", | ||
| input: z10.object({ id: z10.string().min(1), value: modelConfigSchema }), | ||
| execute: async (client, input) => api(client).put(modelPath(input.id), input.value) | ||
| }, | ||
| { | ||
| name: "nl_search_models.delete", | ||
| summary: "Delete a natural language search model", | ||
| category: "nl_search_models", | ||
| input: z10.object({ id: z10.string().min(1) }), | ||
| execute: async (client, input) => api(client).delete(modelPath(input.id)) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/overrides.ts | ||
| import { z as z11 } from "zod"; | ||
| var overridesOperations = [ | ||
| { | ||
| name: "overrides.list", | ||
| summary: "List overrides", | ||
| category: "overrides", | ||
| input: z11.object({ collection: z11.string() }), | ||
| execute: async (client, input) => api(client).get(`${collectionPath(input.collection)}/overrides`) | ||
| }, | ||
| { | ||
| name: "overrides.create", | ||
| summary: "Create or upsert a override", | ||
| category: "overrides", | ||
| input: z11.object({ | ||
| collection: z11.string(), | ||
| name: z11.string(), | ||
| value: z11.record(z11.unknown()) | ||
| }), | ||
| execute: async (client, input) => api(client).put( | ||
| `${collectionPath(input.collection)}/overrides/${enc(input.name)}`, | ||
| input.value | ||
| ) | ||
| }, | ||
| { | ||
| name: "overrides.retrieve", | ||
| summary: "Retrieve a override", | ||
| category: "overrides", | ||
| input: z11.object({ collection: z11.string(), name: z11.string() }), | ||
| execute: async (client, input) => api(client).get( | ||
| `${collectionPath(input.collection)}/overrides/${enc(input.name)}` | ||
| ) | ||
| }, | ||
| { | ||
| name: "overrides.delete", | ||
| summary: "Delete a override", | ||
| category: "overrides", | ||
| input: z11.object({ collection: z11.string(), name: z11.string() }), | ||
| execute: async (client, input) => api(client).delete( | ||
| `${collectionPath(input.collection)}/overrides/${enc(input.name)}` | ||
| ) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/presets.ts | ||
| import { z as z12 } from "zod"; | ||
| var presetsOperations = [ | ||
| { | ||
| name: "presets.list", | ||
| summary: "List presets", | ||
| category: "presets", | ||
| input: z12.object({}), | ||
| execute: async (client) => api(client).get("/presets") | ||
| }, | ||
| { | ||
| name: "presets.create", | ||
| summary: "Create or upsert a preset", | ||
| category: "presets", | ||
| input: z12.object({ | ||
| name: z12.string(), | ||
| value: z12.record(z12.unknown()) | ||
| }), | ||
| execute: async (client, input) => api(client).put(`/presets/${enc(input.name)}`, { | ||
| value: input.value | ||
| }) | ||
| }, | ||
| { | ||
| name: "presets.retrieve", | ||
| summary: "Retrieve a preset", | ||
| category: "presets", | ||
| input: z12.object({ name: z12.string() }), | ||
| execute: async (client, input) => api(client).get(`/presets/${enc(input.name)}`) | ||
| }, | ||
| { | ||
| name: "presets.delete", | ||
| summary: "Delete a preset", | ||
| category: "presets", | ||
| input: z12.object({ name: z12.string() }), | ||
| execute: async (client, input) => api(client).delete(`/presets/${enc(input.name)}`) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/search.ts | ||
| import { z as z13 } from "zod"; | ||
| var searchParams2 = z13.record(z13.unknown()); | ||
| var facetBySchema = z13.union([z13.string().min(1), z13.array(z13.string().min(1))]); | ||
| function withoutUndefined(params) { | ||
| return Object.fromEntries( | ||
| Object.entries(params).filter(([, value]) => value !== void 0) | ||
| ); | ||
| } | ||
| var searchOperations = [ | ||
| { | ||
| name: "search", | ||
| summary: "Search a collection", | ||
| category: "search", | ||
| input: z13.object({ collection: z13.string(), params: searchParams2 }), | ||
| execute: async (client, input) => api(client).get( | ||
| `${collectionPath(input.collection)}/documents/search`, | ||
| input.params | ||
| ) | ||
| }, | ||
| { | ||
| name: "multi_search", | ||
| summary: "Run a Typesense multi-search", | ||
| category: "search", | ||
| input: z13.object({ | ||
| searches: z13.array(searchParams2), | ||
| commonParams: searchParams2.optional() | ||
| }), | ||
| execute: async (client, input) => api(client).post( | ||
| "/multi_search", | ||
| { searches: input.searches }, | ||
| input.commonParams | ||
| ) | ||
| }, | ||
| { | ||
| name: "search.facets", | ||
| summary: "Explore facet counts for a collection", | ||
| category: "search", | ||
| input: z13.object({ | ||
| collection: z13.string(), | ||
| facetBy: facetBySchema, | ||
| q: z13.string().optional().default("*"), | ||
| queryBy: z13.string().optional(), | ||
| filterBy: z13.string().optional(), | ||
| maxFacetValues: z13.number().int().positive().optional(), | ||
| perPage: z13.number().int().nonnegative().optional().default(0) | ||
| }), | ||
| execute: async (client, input) => api(client).get( | ||
| `${collectionPath(input.collection)}/documents/search`, | ||
| withoutUndefined({ | ||
| q: input.q, | ||
| query_by: input.queryBy, | ||
| filter_by: input.filterBy, | ||
| facet_by: Array.isArray(input.facetBy) ? input.facetBy.join(",") : input.facetBy, | ||
| max_facet_values: input.maxFacetValues, | ||
| per_page: input.perPage | ||
| }) | ||
| ) | ||
| }, | ||
| { | ||
| name: "search.suggestions", | ||
| summary: "Fetch prefix search suggestions from a collection", | ||
| category: "search", | ||
| input: z13.object({ | ||
| collection: z13.string(), | ||
| q: z13.string(), | ||
| queryBy: z13.string(), | ||
| filterBy: z13.string().optional(), | ||
| includeFields: z13.union([z13.string(), z13.array(z13.string())]).optional(), | ||
| limit: z13.number().int().positive().max(50).optional().default(5), | ||
| prefix: z13.boolean().optional().default(true) | ||
| }), | ||
| execute: async (client, input) => api(client).get( | ||
| `${collectionPath(input.collection)}/documents/search`, | ||
| withoutUndefined({ | ||
| q: input.q, | ||
| query_by: input.queryBy, | ||
| filter_by: input.filterBy, | ||
| include_fields: Array.isArray(input.includeFields) ? input.includeFields.join(",") : input.includeFields, | ||
| per_page: input.limit, | ||
| prefix: input.prefix | ||
| }) | ||
| ) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/stemming.ts | ||
| import { z as z14 } from "zod"; | ||
| var wordMappingSchema = z14.object({ | ||
| word: z14.string().min(1), | ||
| root: z14.string().min(1) | ||
| }); | ||
| var stemmingOperations = [ | ||
| { | ||
| name: "stemming.dictionaries.list", | ||
| summary: "List stemming dictionaries", | ||
| category: "stemming", | ||
| input: z14.object({}), | ||
| execute: async (client) => api(client).get("/stemming/dictionaries") | ||
| }, | ||
| { | ||
| name: "stemming.dictionaries.retrieve", | ||
| summary: "Retrieve a stemming dictionary", | ||
| category: "stemming", | ||
| input: z14.object({ id: z14.string().min(1) }), | ||
| execute: async (client, input) => api(client).get(`/stemming/dictionaries/${enc(input.id)}`) | ||
| }, | ||
| { | ||
| name: "stemming.dictionaries.import", | ||
| summary: "Import or replace a stemming dictionary from word mappings", | ||
| category: "stemming", | ||
| input: z14.object({ | ||
| id: z14.string().min(1), | ||
| words: z14.union([z14.string().min(1), z14.array(wordMappingSchema).min(1)]) | ||
| }), | ||
| execute: async (client, input) => api(client).post( | ||
| "/stemming/dictionaries/import", | ||
| Array.isArray(input.words) ? input.words.map( | ||
| (mapping) => JSON.stringify(mapping) | ||
| ).join("\n") : input.words, | ||
| { id: input.id } | ||
| ) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/stopwords.ts | ||
| import { z as z15 } from "zod"; | ||
| var stopwordsOperations = [ | ||
| { | ||
| name: "stopwords.list", | ||
| summary: "List stopwords", | ||
| category: "stopwords", | ||
| input: z15.object({}), | ||
| execute: async (client) => api(client).get("/stopwords") | ||
| }, | ||
| { | ||
| name: "stopwords.create", | ||
| summary: "Create or upsert a stopword", | ||
| category: "stopwords", | ||
| input: z15.object({ | ||
| name: z15.string(), | ||
| value: z15.record(z15.unknown()) | ||
| }), | ||
| execute: async (client, input) => api(client).put(`/stopwords/${enc(input.name)}`, input.value) | ||
| }, | ||
| { | ||
| name: "stopwords.retrieve", | ||
| summary: "Retrieve a stopword", | ||
| category: "stopwords", | ||
| input: z15.object({ name: z15.string() }), | ||
| execute: async (client, input) => api(client).get(`/stopwords/${enc(input.name)}`) | ||
| }, | ||
| { | ||
| name: "stopwords.delete", | ||
| summary: "Delete a stopword", | ||
| category: "stopwords", | ||
| input: z15.object({ name: z15.string() }), | ||
| execute: async (client, input) => api(client).delete(`/stopwords/${enc(input.name)}`) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/synonym-sets.ts | ||
| import { z as z16 } from "zod"; | ||
| function base2(name) { | ||
| return name ? `/synonym_sets/${enc(name)}` : "/synonym_sets"; | ||
| } | ||
| function itemPath2(name, id) { | ||
| const path = `${base2(name)}/items`; | ||
| return id ? `${path}/${enc(id)}` : path; | ||
| } | ||
| var synonymSetOperations = [ | ||
| { | ||
| name: "synonym_sets.list", | ||
| summary: "List global synonym sets", | ||
| category: "synonyms", | ||
| input: z16.object({}), | ||
| execute: async (client) => api(client).get(base2()) | ||
| }, | ||
| { | ||
| name: "synonym_sets.create", | ||
| summary: "Create or upsert a global synonym set", | ||
| category: "synonyms", | ||
| input: z16.object({ | ||
| name: z16.string(), | ||
| value: z16.object({ | ||
| items: z16.array(z16.record(z16.unknown())) | ||
| }) | ||
| }), | ||
| execute: async (client, input) => api(client).put(base2(input.name), input.value) | ||
| }, | ||
| { | ||
| name: "synonym_sets.retrieve", | ||
| summary: "Retrieve a global synonym set", | ||
| category: "synonyms", | ||
| input: z16.object({ name: z16.string() }), | ||
| execute: async (client, input) => api(client).get(base2(input.name)) | ||
| }, | ||
| { | ||
| name: "synonym_sets.delete", | ||
| summary: "Delete a global synonym set", | ||
| category: "synonyms", | ||
| input: z16.object({ name: z16.string() }), | ||
| execute: async (client, input) => api(client).delete(base2(input.name)) | ||
| }, | ||
| { | ||
| name: "synonym_sets.items.list", | ||
| summary: "List items in a global synonym set", | ||
| category: "synonyms", | ||
| input: z16.object({ name: z16.string() }), | ||
| execute: async (client, input) => api(client).get(itemPath2(input.name)) | ||
| }, | ||
| { | ||
| name: "synonym_sets.items.create", | ||
| summary: "Create or upsert an item in a global synonym set", | ||
| category: "synonyms", | ||
| input: z16.object({ | ||
| name: z16.string(), | ||
| id: z16.string(), | ||
| value: z16.record(z16.unknown()) | ||
| }), | ||
| execute: async (client, input) => api(client).put(itemPath2(input.name, input.id), input.value) | ||
| }, | ||
| { | ||
| name: "synonym_sets.items.retrieve", | ||
| summary: "Retrieve an item in a global synonym set", | ||
| category: "synonyms", | ||
| input: z16.object({ name: z16.string(), id: z16.string() }), | ||
| execute: async (client, input) => api(client).get(itemPath2(input.name, input.id)) | ||
| }, | ||
| { | ||
| name: "synonym_sets.items.delete", | ||
| summary: "Delete an item in a global synonym set", | ||
| category: "synonyms", | ||
| input: z16.object({ name: z16.string(), id: z16.string() }), | ||
| execute: async (client, input) => api(client).delete(itemPath2(input.name, input.id)) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/synonyms.ts | ||
| import { z as z17 } from "zod"; | ||
| function isNotFound(error) { | ||
| if (typeof error !== "object" || error === null) return false; | ||
| const { httpStatus, status } = error; | ||
| return httpStatus === 404 || status === 404; | ||
| } | ||
| function synonymSetNames(collection) { | ||
| return Array.isArray(collection.synonym_sets) ? collection.synonym_sets.filter( | ||
| (name) => typeof name === "string" | ||
| ) : []; | ||
| } | ||
| function globalSynonymGuidance(collection, sets) { | ||
| return [ | ||
| `Collection-level synonyms are unavailable for ${collection}.`, | ||
| sets.length > 0 ? `This collection is linked to global synonym sets: ${sets.join(", ")}.` : "This Typesense version may use global synonym sets.", | ||
| "Use synonym_sets.list to inspect global synonym sets:", | ||
| "tsk synonym_sets.list --input '{}' --json" | ||
| ].join("\n"); | ||
| } | ||
| var synonymsOperations = [ | ||
| { | ||
| name: "synonyms.list", | ||
| summary: "List synonyms", | ||
| category: "synonyms", | ||
| input: z17.object({ collection: z17.string() }), | ||
| execute: async (client, input) => { | ||
| const request = api(client); | ||
| try { | ||
| return await request.get( | ||
| `${collectionPath(input.collection)}/synonyms` | ||
| ); | ||
| } catch (error) { | ||
| if (!isNotFound(error)) throw error; | ||
| const collection = await request.get( | ||
| collectionPath(input.collection) | ||
| ); | ||
| throw new Error( | ||
| globalSynonymGuidance(input.collection, synonymSetNames(collection)) | ||
| ); | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| name: "synonyms.create", | ||
| summary: "Create or upsert a synonym", | ||
| category: "synonyms", | ||
| input: z17.object({ | ||
| collection: z17.string(), | ||
| name: z17.string(), | ||
| value: z17.record(z17.unknown()) | ||
| }), | ||
| execute: async (client, input) => api(client).put( | ||
| `${collectionPath(input.collection)}/synonyms/${enc(input.name)}`, | ||
| input.value | ||
| ) | ||
| }, | ||
| { | ||
| name: "synonyms.retrieve", | ||
| summary: "Retrieve a synonym", | ||
| category: "synonyms", | ||
| input: z17.object({ collection: z17.string(), name: z17.string() }), | ||
| execute: async (client, input) => api(client).get( | ||
| `${collectionPath(input.collection)}/synonyms/${enc(input.name)}` | ||
| ) | ||
| }, | ||
| { | ||
| name: "synonyms.delete", | ||
| summary: "Delete a synonym", | ||
| category: "synonyms", | ||
| input: z17.object({ collection: z17.string(), name: z17.string() }), | ||
| execute: async (client, input) => api(client).delete( | ||
| `${collectionPath(input.collection)}/synonyms/${enc(input.name)}` | ||
| ) | ||
| } | ||
| ]; | ||
| // ../core/src/operations/system.ts | ||
| import { z as z18 } from "zod"; | ||
| var systemOperations = [ | ||
| { | ||
| name: "operations.schema_changes", | ||
| summary: "List in-progress collection schema changes", | ||
| category: "system", | ||
| input: z18.object({}), | ||
| execute: async (client) => api(client).get("/operations/schema_changes") | ||
| }, | ||
| { | ||
| name: "operations.snapshot", | ||
| summary: "Create a point-in-time server snapshot", | ||
| category: "system", | ||
| input: z18.object({ snapshotPath: z18.string().min(1) }), | ||
| execute: async (client, input) => api(client).post("/operations/snapshot", void 0, { | ||
| snapshot_path: input.snapshotPath | ||
| }) | ||
| }, | ||
| { | ||
| name: "operations.vote", | ||
| summary: "Trigger leader re-election on a follower node", | ||
| category: "system", | ||
| input: z18.object({}), | ||
| execute: async (client) => api(client).post("/operations/vote") | ||
| }, | ||
| { | ||
| name: "operations.cache.clear", | ||
| summary: "Clear cached search responses", | ||
| category: "system", | ||
| input: z18.object({}), | ||
| execute: async (client) => api(client).post("/operations/cache/clear") | ||
| }, | ||
| { | ||
| name: "operations.db.compact", | ||
| summary: "Compact the on-disk Typesense database", | ||
| category: "system", | ||
| input: z18.object({}), | ||
| execute: async (client) => api(client).post("/operations/db/compact") | ||
| }, | ||
| { | ||
| name: "operations.slow_requests.configure", | ||
| summary: "Configure the slow-request logging threshold", | ||
| category: "system", | ||
| input: z18.object({ thresholdMs: z18.number().int().min(-1) }), | ||
| execute: async (client, input) => api(client).post("/config", { | ||
| "log-slow-requests-time-ms": input.thresholdMs | ||
| }) | ||
| }, | ||
| { | ||
| name: "health", | ||
| summary: "Check Typesense cluster health", | ||
| category: "system", | ||
| input: z18.object({}), | ||
| execute: async (client) => api(client).get("/health") | ||
| }, | ||
| { | ||
| name: "metrics", | ||
| summary: "Retrieve Typesense metrics", | ||
| category: "system", | ||
| input: z18.object({}), | ||
| execute: async (client) => api(client).get("/metrics.json") | ||
| }, | ||
| { | ||
| name: "stats", | ||
| summary: "Retrieve Typesense stats", | ||
| category: "system", | ||
| input: z18.object({}), | ||
| execute: async (client) => api(client).get("/stats.json") | ||
| }, | ||
| { | ||
| name: "debug", | ||
| summary: "Retrieve Typesense debug info", | ||
| category: "system", | ||
| input: z18.object({}), | ||
| execute: async (client) => api(client).get("/debug") | ||
| } | ||
| ]; | ||
| // ../core/src/operations/index.ts | ||
| var operations = [ | ||
| ...collectionOperations, | ||
| ...documentOperations, | ||
| ...searchOperations, | ||
| ...aliasesOperations, | ||
| ...synonymsOperations, | ||
| ...synonymSetOperations, | ||
| ...curationSetOperations, | ||
| ...overridesOperations, | ||
| ...keysOperations, | ||
| ...analyticsOperations, | ||
| ...presetsOperations, | ||
| ...stopwordsOperations, | ||
| ...stemmingOperations, | ||
| ...conversationOperations, | ||
| ...nlSearchModelOperations, | ||
| ...apiOperations, | ||
| ...systemOperations | ||
| ]; | ||
| // package.json | ||
| var package_default = { | ||
| name: "@typesensekit/mcp", | ||
| version: "1.2.0", | ||
| type: "module", | ||
| main: "dist/server.js", | ||
| types: "dist/server.d.ts", | ||
| bin: { | ||
| "typesensekit-mcp": "dist/cli.js", | ||
| "typesensekit-mcp-http": "dist/http.js" | ||
| }, | ||
| exports: { | ||
| ".": { | ||
| types: "./dist/server.d.ts", | ||
| import: "./dist/server.js" | ||
| } | ||
| }, | ||
| files: [ | ||
| "dist", | ||
| "README.md" | ||
| ], | ||
| scripts: { | ||
| build: "tsup", | ||
| dev: "tsup --watch", | ||
| typecheck: "tsc --noEmit", | ||
| test: "vitest run" | ||
| }, | ||
| dependencies: { | ||
| "@modelcontextprotocol/sdk": "^1.29.0", | ||
| zod: "^3.25.76", | ||
| typesense: "^3.0.6" | ||
| }, | ||
| devDependencies: { | ||
| tsup: "^8.5.1", | ||
| "@types/node": "^22.10.0", | ||
| "@typesensekit/core": "workspace:*" | ||
| }, | ||
| publishConfig: { | ||
| access: "public" | ||
| }, | ||
| description: "MCP stdio server exposing Typesense API operations as tools.", | ||
| license: "MIT", | ||
| author: "Akshit Kr Nagpal", | ||
| repository: { | ||
| type: "git", | ||
| url: "git+https://github.com/akshitkrnagpal/typesensekit.git", | ||
| directory: "packages/mcp" | ||
| }, | ||
| bugs: { | ||
| url: "https://github.com/akshitkrnagpal/typesensekit/issues" | ||
| }, | ||
| homepage: "https://github.com/akshitkrnagpal/typesensekit#readme", | ||
| keywords: [ | ||
| "typesense", | ||
| "cli", | ||
| "mcp", | ||
| "model-context-protocol", | ||
| "search" | ||
| ] | ||
| }; | ||
| // src/read-only.ts | ||
| var READ_ONLY_OPERATION_NAMES = /* @__PURE__ */ new Set([ | ||
| "aliases.list", | ||
| "aliases.retrieve", | ||
| "analytics.events.list", | ||
| "analytics.rules.list", | ||
| "analytics.rules.retrieve", | ||
| "analytics.status", | ||
| "collections.list", | ||
| "collections.retrieve", | ||
| "collections.wait", | ||
| "conversations.history.retrieve", | ||
| "conversations.models.list", | ||
| "conversations.models.retrieve", | ||
| "curation_sets.items.list", | ||
| "curation_sets.items.retrieve", | ||
| "curation_sets.list", | ||
| "curation_sets.retrieve", | ||
| "debug", | ||
| "documents.export", | ||
| "documents.get", | ||
| "documents.get_many", | ||
| "documents.search", | ||
| "health", | ||
| "metrics", | ||
| "multi_search", | ||
| "nl_search_models.list", | ||
| "nl_search_models.retrieve", | ||
| "overrides.list", | ||
| "overrides.retrieve", | ||
| "operations.schema_changes", | ||
| "presets.list", | ||
| "presets.retrieve", | ||
| "search", | ||
| "search.facets", | ||
| "search.suggestions", | ||
| "stats", | ||
| "stemming.dictionaries.list", | ||
| "stemming.dictionaries.retrieve", | ||
| "stopwords.list", | ||
| "stopwords.retrieve", | ||
| "synonym_sets.items.list", | ||
| "synonym_sets.items.retrieve", | ||
| "synonym_sets.list", | ||
| "synonym_sets.retrieve", | ||
| "synonyms.list", | ||
| "synonyms.retrieve" | ||
| ]); | ||
| function isReadOnlyOperation(operation) { | ||
| return READ_ONLY_OPERATION_NAMES.has(operation.name); | ||
| } | ||
| function filterMcpOperations(operations2, readOnly) { | ||
| return readOnly ? operations2.filter(isReadOnlyOperation) : operations2; | ||
| } | ||
| function readOnlyFromEnv(value) { | ||
| if (value === void 0) return true; | ||
| return !["0", "false", "no", "off"].includes(value.toLowerCase()); | ||
| } | ||
| // src/env.ts | ||
| function readEnvConfig() { | ||
| return serverConfigSchema.parse({ | ||
| url: process.env.TYPESENSE_URL, | ||
| apiKey: process.env.TYPESENSE_API_KEY, | ||
| connectionTimeoutSeconds: process.env.TYPESENSE_CONNECTION_TIMEOUT_SECONDS ? Number(process.env.TYPESENSE_CONNECTION_TIMEOUT_SECONDS) : void 0 | ||
| }); | ||
| } | ||
| function readMcpOptions() { | ||
| return { | ||
| readOnly: readOnlyFromEnv(process.env.TYPESENSEKIT_READ_ONLY) | ||
| }; | ||
| } | ||
| // src/resources.ts | ||
| import { | ||
| ResourceTemplate | ||
| } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| function jsonContents(uri, value) { | ||
| return { | ||
| contents: [ | ||
| { | ||
| uri, | ||
| mimeType: "application/json", | ||
| text: JSON.stringify(value, null, 2) | ||
| } | ||
| ] | ||
| }; | ||
| } | ||
| function textContents(uri, value) { | ||
| return { | ||
| contents: [{ uri, mimeType: "text/plain", text: value }] | ||
| }; | ||
| } | ||
| function operationSummary(operation) { | ||
| return { | ||
| name: operation.name, | ||
| summary: operation.summary, | ||
| category: operation.category, | ||
| readOnly: READ_ONLY_OPERATION_NAMES.has(operation.name) | ||
| }; | ||
| } | ||
| function operationManifest(activeOperations, readOnly) { | ||
| return { | ||
| readOnly, | ||
| operations: activeOperations.map(operationSummary) | ||
| }; | ||
| } | ||
| function singleVariable(value, variable) { | ||
| if (typeof value === "string") return value; | ||
| throw new Error(`Missing ${variable} resource variable`); | ||
| } | ||
| async function readOperationResource(client, operationName, input, uri) { | ||
| const operation = operations.find( | ||
| (candidate) => candidate.name === operationName | ||
| ); | ||
| if (!operation) throw new Error(`${operationName} not found`); | ||
| try { | ||
| const result = await operation.execute( | ||
| client, | ||
| operation.input.parse(input) | ||
| ); | ||
| return jsonContents(uri, result); | ||
| } catch (error) { | ||
| return textContents(uri, formatTypesenseErrorMessage(error)); | ||
| } | ||
| } | ||
| function registerTypesenseResources(server, client, activeOperations, readOnly) { | ||
| server.registerResource( | ||
| "typesensekit-operations", | ||
| "typesensekit://operations", | ||
| { | ||
| title: "TypesenseKit Operations", | ||
| description: "Operations currently exposed by this MCP server.", | ||
| mimeType: "application/json" | ||
| }, | ||
| async (uri) => jsonContents(uri.href, operationManifest(activeOperations, readOnly)) | ||
| ); | ||
| server.registerResource( | ||
| "typesensekit-read-only-tools", | ||
| "typesensekit://read-only-tools", | ||
| { | ||
| title: "TypesenseKit Read-only Tools", | ||
| description: "Operation names included in default read-only MCP mode.", | ||
| mimeType: "application/json" | ||
| }, | ||
| async (uri) => jsonContents(uri.href, { | ||
| operations: [...READ_ONLY_OPERATION_NAMES].sort() | ||
| }) | ||
| ); | ||
| server.registerResource( | ||
| "typesense-collection-schema", | ||
| new ResourceTemplate("typesense://collections/{collection}/schema", { | ||
| list: void 0 | ||
| }), | ||
| { | ||
| title: "Typesense Collection Schema", | ||
| description: "Retrieve a Typesense collection schema by collection name.", | ||
| mimeType: "application/json" | ||
| }, | ||
| async (uri, variables) => readOperationResource( | ||
| client, | ||
| "collections.retrieve", | ||
| { collection: singleVariable(variables.collection, "collection") }, | ||
| uri.href | ||
| ) | ||
| ); | ||
| server.registerResource( | ||
| "typesense-document", | ||
| new ResourceTemplate( | ||
| "typesense://collections/{collection}/documents/{id}", | ||
| { | ||
| list: void 0 | ||
| } | ||
| ), | ||
| { | ||
| title: "Typesense Document", | ||
| description: "Retrieve a Typesense document by collection and document id.", | ||
| mimeType: "application/json" | ||
| }, | ||
| async (uri, variables) => readOperationResource( | ||
| client, | ||
| "documents.get", | ||
| { | ||
| collection: singleVariable(variables.collection, "collection"), | ||
| id: singleVariable(variables.id, "id") | ||
| }, | ||
| uri.href | ||
| ) | ||
| ); | ||
| } | ||
| // src/server.ts | ||
| function toToolShape(input) { | ||
| const objectInput = input; | ||
| return objectInput.shape; | ||
| } | ||
| function createTypesenseMcpServer(options = {}) { | ||
| const server = new McpServer({ | ||
| name: "typesensekit", | ||
| version: package_default.version | ||
| }); | ||
| const client = createClient(readEnvConfig()); | ||
| const mcpOptions = { ...readMcpOptions(), ...options }; | ||
| const activeOperations = filterMcpOperations(operations, mcpOptions.readOnly); | ||
| registerTypesenseResources( | ||
| server, | ||
| client, | ||
| activeOperations, | ||
| mcpOptions.readOnly | ||
| ); | ||
| for (const operation of activeOperations) { | ||
| server.registerTool( | ||
| operation.name, | ||
| { | ||
| title: operation.name, | ||
| description: operation.summary, | ||
| inputSchema: toToolShape(operation.input) | ||
| }, | ||
| async (args) => { | ||
| try { | ||
| const input = operation.input.parse(args); | ||
| const result = await operation.execute(client, input); | ||
| return { | ||
| content: [ | ||
| { | ||
| type: "text", | ||
| text: JSON.stringify(redactSecrets(result), null, 2) | ||
| } | ||
| ] | ||
| }; | ||
| } catch (error) { | ||
| const message = formatTypesenseErrorMessage(error); | ||
| return { isError: true, content: [{ type: "text", text: message }] }; | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| return server; | ||
| } | ||
| export { | ||
| createTypesenseMcpServer | ||
| }; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
137966
12.24%4235
11.95%16
14.29%