Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mcp-resources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@typesensekit/mcp": patch
---

Expose MCP resources for operation discovery, read-only tool discovery, collection schemas, and document lookup.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions packages/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
40 changes: 40 additions & 0 deletions packages/mcp/src/resources.test.ts
Original file line number Diff line number Diff line change
@@ -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,
},
],
});
});
});
162 changes: 162 additions & 0 deletions packages/mcp/src/resources.ts
Original file line number Diff line number Diff line change
@@ -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<z.ZodTypeAny, unknown>) {
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,
),
);
}
13 changes: 10 additions & 3 deletions packages/mcp/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<z.ZodRawShape>;

Expand All @@ -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,
{
Expand Down
Loading