From 6243ca31ccdc2683e664f3a725520f6685be494b Mon Sep 17 00:00:00 2001 From: akshitkrnagpal Date: Mon, 22 Jun 2026 02:12:46 +0400 Subject: [PATCH] Expose MCP resources --- .changeset/mcp-resources.md | 5 + README.md | 9 ++ packages/mcp/README.md | 4 +- packages/mcp/src/resources.test.ts | 40 +++++++ packages/mcp/src/resources.ts | 162 +++++++++++++++++++++++++++++ packages/mcp/src/server.ts | 13 ++- 6 files changed, 228 insertions(+), 5 deletions(-) create mode 100644 .changeset/mcp-resources.md create mode 100644 packages/mcp/src/resources.test.ts create mode 100644 packages/mcp/src/resources.ts diff --git a/.changeset/mcp-resources.md b/.changeset/mcp-resources.md new file mode 100644 index 0000000..cf8d10a --- /dev/null +++ b/.changeset/mcp-resources.md @@ -0,0 +1,5 @@ +--- +"@typesensekit/mcp": patch +--- + +Expose MCP resources for operation discovery, read-only tool discovery, collection schemas, and document lookup. diff --git a/README.md b/README.md index 9dc3eff..6e32d7c 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,15 @@ tsk skills hermes For scoped key examples, production guidance, and the compatibility matrix, read [`docs/mcp-security.md`](./docs/mcp-security.md). +The MCP server also exposes resources: + +| Resource | Purpose | +| --- | --- | +| `typesensekit://operations` | JSON manifest of operations exposed by the current MCP mode | +| `typesensekit://read-only-tools` | JSON list of tools included in default read-only mode | +| `typesense://collections/{collection}/schema` | Collection schema lookup | +| `typesense://collections/{collection}/documents/{id}` | Document lookup | + ## API Coverage TypesenseKit covers the common Typesense administration and search surfaces, plus `api.call` for endpoints that are new, uncommon, or not yet wrapped. diff --git a/packages/mcp/README.md b/packages/mcp/README.md index be5a4cd..333a0e3 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -9,5 +9,5 @@ TYPESENSE_URL=http://localhost:8108 TYPESENSE_API_KEY=xyz pnpm dlx @typesensekit Set `TYPESENSEKIT_READ_ONLY=false` to expose write/delete/admin tools. -See the root README for client configuration, security guidance, and operation -coverage. +See the root README for client configuration, MCP resources, security guidance, +and operation coverage. diff --git a/packages/mcp/src/resources.test.ts b/packages/mcp/src/resources.test.ts new file mode 100644 index 0000000..49134eb --- /dev/null +++ b/packages/mcp/src/resources.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; +import { operationManifest } from "./resources.js"; + +describe("MCP resources", () => { + it("summarizes active operations for the operation manifest resource", () => { + expect( + operationManifest( + [ + { + name: "search", + summary: "Search a collection", + category: "search", + }, + { + name: "documents.index", + summary: "Index a document", + category: "documents", + }, + ] as never[], + true, + ), + ).toEqual({ + readOnly: true, + operations: [ + { + name: "search", + summary: "Search a collection", + category: "search", + readOnly: true, + }, + { + name: "documents.index", + summary: "Index a document", + category: "documents", + readOnly: false, + }, + ], + }); + }); +}); diff --git a/packages/mcp/src/resources.ts b/packages/mcp/src/resources.ts new file mode 100644 index 0000000..6e48b0f --- /dev/null +++ b/packages/mcp/src/resources.ts @@ -0,0 +1,162 @@ +import { + type McpServer, + ResourceTemplate, +} from "@modelcontextprotocol/sdk/server/mcp.js"; +import { + formatTypesenseErrorMessage, + type Operation, + operations, + type TypesenseClient, +} from "@typesensekit/core"; +import type { z } from "zod"; +import { type McpOperation, READ_ONLY_OPERATION_NAMES } from "./read-only.js"; + +type ResourceOperationInput = { + collection: string; + id?: string; +}; + +function jsonContents(uri: string, value: unknown) { + return { + contents: [ + { + uri, + mimeType: "application/json", + text: JSON.stringify(value, null, 2), + }, + ], + }; +} + +function textContents(uri: string, value: string) { + return { + contents: [{ uri, mimeType: "text/plain", text: value }], + }; +} + +function operationSummary(operation: Operation) { + return { + name: operation.name, + summary: operation.summary, + category: operation.category, + readOnly: READ_ONLY_OPERATION_NAMES.has(operation.name), + }; +} + +export function operationManifest( + activeOperations: McpOperation[], + readOnly: boolean, +) { + return { + readOnly, + operations: activeOperations.map(operationSummary), + }; +} + +function singleVariable( + value: string | string[] | undefined, + variable: string, +): string { + if (typeof value === "string") return value; + throw new Error(`Missing ${variable} resource variable`); +} + +async function readOperationResource( + client: TypesenseClient, + operationName: string, + input: ResourceOperationInput, + uri: string, +) { + 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)); + } +} + +export function registerTypesenseResources( + server: McpServer, + client: TypesenseClient, + activeOperations: McpOperation[], + readOnly: boolean, +) { + 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: undefined, + }), + { + 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: undefined, + }, + ), + { + 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, + ), + ); +} diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts index 1f5ff2a..bf71d1d 100644 --- a/packages/mcp/src/server.ts +++ b/packages/mcp/src/server.ts @@ -9,6 +9,7 @@ import type { z } from "zod"; import packageJson from "../package.json" with { type: "json" }; import { readEnvConfig, readMcpOptions } from "./env.js"; import { filterMcpOperations } from "./read-only.js"; +import { registerTypesenseResources } from "./resources.js"; type ToolShape = z.ZodObject; @@ -31,10 +32,16 @@ export function createTypesenseMcpServer( const client = createClient(readEnvConfig()); const mcpOptions = { ...readMcpOptions(), ...options }; - for (const operation of filterMcpOperations( - operations, + const activeOperations = filterMcpOperations(operations, mcpOptions.readOnly); + + registerTypesenseResources( + server, + client, + activeOperations, mcpOptions.readOnly, - )) { + ); + + for (const operation of activeOperations) { server.registerTool( operation.name, {