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-http-docker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@typesensekit/mcp": patch
---

Add a stateless Streamable HTTP MCP entrypoint plus Docker and end-to-end assistant search documentation.
10 changes: 10 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
node_modules
dist
coverage
.git
.turbo
.vite
.env
.env.*
*.log
*.tgz
20 changes: 20 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM node:24-slim

WORKDIR /app

RUN corepack enable
ENV CI=true

COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json ./
COPY packages ./packages

RUN pnpm install --frozen-lockfile \
&& pnpm --filter @typesensekit/mcp build

ENV TYPESENSEKIT_MCP_PORT=3000
ENV TYPESENSEKIT_READ_ONLY=true
EXPOSE 3000

USER node

CMD ["node", "packages/mcp/dist/http.js"]
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ tsk skills hermes
For scoped key examples, production guidance, and the compatibility matrix, read
[`docs/mcp-security.md`](./docs/mcp-security.md).

For Streamable HTTP, Docker, and deployment examples, read
[`docs/mcp-http-docker.md`](./docs/mcp-http-docker.md). For an end-to-end
assistant search flow with citations, read
[`examples/assistant-search-citations.md`](./examples/assistant-search-citations.md).

The MCP server also exposes resources:

| Resource | Purpose |
Expand Down
71 changes: 71 additions & 0 deletions docs/mcp-http-docker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# MCP HTTP and Docker

TypesenseKit ships two MCP entrypoints:

- `typesensekit-mcp` for local stdio clients.
- `typesensekit-mcp-http` for stateless Streamable HTTP deployments.

## Run HTTP Locally

```sh
TYPESENSE_URL=http://localhost:8108 \
TYPESENSE_API_KEY=xyz \
TYPESENSEKIT_MCP_PORT=3000 \
pnpm --filter @typesensekit/mcp exec typesensekit-mcp-http
```

The HTTP endpoint defaults to `POST /mcp`. A lightweight health check is
available at `GET /healthz`.

Optional environment:

```sh
TYPESENSEKIT_MCP_PATH=/mcp
TYPESENSEKIT_READ_ONLY=true
TYPESENSE_CONNECTION_TIMEOUT_SECONDS=5
```

## Docker

Build the image:

```sh
docker build -t typesensekit-mcp .
```

Run the image:

```sh
docker run --rm -p 3000:3000 \
-e TYPESENSE_URL=https://your-cluster.typesense.net \
-e TYPESENSE_API_KEY=your-scoped-api-key \
-e TYPESENSEKIT_READ_ONLY=true \
typesensekit-mcp
```

## Minimal Deploy Example

Deploy the container behind a platform or reverse proxy that provides TLS,
request logging, and rate limiting:

```yaml
services:
- name: typesensekit-mcp
type: web
env: docker
dockerfilePath: ./Dockerfile
healthCheckPath: /healthz
envVars:
- key: TYPESENSE_URL
sync: false
- key: TYPESENSE_API_KEY
sync: false
- key: TYPESENSEKIT_READ_ONLY
value: "true"
- key: TYPESENSE_CONNECTION_TIMEOUT_SECONDS
value: "5"
```

Keep the service private unless the client performs authentication in front of
it. TypesenseKit's HTTP entrypoint does not add its own authentication layer; it
relies on the deployment boundary and the scoped Typesense API key.
67 changes: 67 additions & 0 deletions examples/assistant-search-citations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Assistant Search With Citations

This example shows the intended assistant flow: connect an MCP client, run a
search, then cite Typesense document fields in the final answer.

## 1. Start TypesenseKit

```sh
TYPESENSE_URL=https://your-cluster.typesense.net \
TYPESENSE_API_KEY=your-scoped-api-key \
npx -y @typesensekit/mcp
```

## 2. Configure The MCP Client

```json
{
"mcpServers": {
"typesensekit": {
"command": "npx",
"args": ["-y", "@typesensekit/mcp"],
"env": {
"TYPESENSE_URL": "https://your-cluster.typesense.net",
"TYPESENSE_API_KEY": "your-scoped-api-key",
"TYPESENSEKIT_READ_ONLY": "true"
}
}
}
}
```

## 3. Ask The Assistant

Prompt:

```text
Search the products collection for "lounge chair". Use documents.search with
query_by set to "title,description" and cite each recommendation with its
document id and URL.
```

Tool call:

```json
{
"collection": "products",
"params": {
"q": "lounge chair",
"query_by": "title,description",
"include_fields": "id,title,url,description",
"per_page": 3
}
}
```

Answer shape:

```text
Recommended matches:

1. Eames-style lounge chair - cited from document prod_123, https://example.com/products/prod_123
2. Walnut reading chair - cited from document prod_456, https://example.com/products/prod_456
3. Low leather lounge chair - cited from document prod_789, https://example.com/products/prod_789
```

For stricter citations, require the assistant to quote only fields returned by
Typesense and include each document `id` or canonical `url`.
6 changes: 6 additions & 0 deletions packages/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,11 @@ TYPESENSE_URL=http://localhost:8108 TYPESENSE_API_KEY=xyz pnpm dlx @typesensekit

Set `TYPESENSEKIT_READ_ONLY=false` to expose write/delete/admin tools.

Run the stateless Streamable HTTP server:

```sh
TYPESENSE_URL=http://localhost:8108 TYPESENSE_API_KEY=xyz pnpm dlx --package @typesensekit/mcp typesensekit-mcp-http
```

See the root README for client configuration, MCP resources, security guidance,
and operation coverage.
3 changes: 2 additions & 1 deletion packages/mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"main": "dist/server.js",
"types": "dist/server.d.ts",
"bin": {
"typesensekit-mcp": "dist/cli.js"
"typesensekit-mcp": "dist/cli.js",
"typesensekit-mcp-http": "dist/http.js"
},
"exports": {
".": {
Expand Down
130 changes: 130 additions & 0 deletions packages/mcp/src/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import {
createServer,
type IncomingMessage,
type ServerResponse,
} from "node:http";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createTypesenseMcpServer } from "./server.js";

const DEFAULT_PORT = 3000;
const DEFAULT_PATH = "/mcp";

function readPort(): number {
const value = process.env.TYPESENSEKIT_MCP_PORT ?? process.env.PORT;
if (!value) return DEFAULT_PORT;

const port = Number(value);
if (!Number.isInteger(port) || port <= 0) {
throw new Error(`Invalid MCP HTTP port: ${value}`);
}
return port;
}

function readPath(): string {
const path = process.env.TYPESENSEKIT_MCP_PATH ?? DEFAULT_PATH;
return path.startsWith("/") ? path : `/${path}`;
}

function sendJson(
res: ServerResponse,
statusCode: number,
body: Record<string, unknown>,
) {
res.writeHead(statusCode, { "content-type": "application/json" });
res.end(JSON.stringify(body));
}

function readBody(req: IncomingMessage): Promise<unknown> {
return new Promise((resolve, reject) => {
let body = "";
req.setEncoding("utf8");
req.on("data", (chunk) => {
body += chunk;
});
req.on("error", reject);
req.on("end", () => {
if (!body) {
resolve(undefined);
return;
}

try {
resolve(JSON.parse(body));
} catch (error) {
reject(error);
}
});
});
}

function jsonRpcError(message: string) {
return {
jsonrpc: "2.0",
error: { code: -32603, message },
id: null,
};
}

async function handleMcpRequest(req: IncomingMessage, res: ServerResponse) {
const server = createTypesenseMcpServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});

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 port = readPort();
const path = readPath();

const 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"));
}
}
});

httpServer.listen(port, () => {
console.error(
`TypesenseKit MCP HTTP server listening on ${path} port ${port}`,
);
});

for (const signal of ["SIGINT", "SIGTERM"] as const) {
process.on(signal, () => {
httpServer.close(() => process.exit(0));
});
}
2 changes: 1 addition & 1 deletion packages/mcp/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { defineConfig } from "tsup";

export default defineConfig([
{
entry: { cli: "src/cli.ts" },
entry: { cli: "src/cli.ts", http: "src/http.ts" },
format: ["esm"],
dts: true,
clean: true,
Expand Down
Loading