From c7c6f587559fb59f097a5eb95230505013c98ccf Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Thu, 4 Jun 2026 16:27:28 +0200 Subject: [PATCH 01/33] feat: generate search_index.json containing all documentation pages --- .../docusaurus-plugin-ai-docs/README.md | 18 +++ .../docusaurus-plugin-ai-docs/index.js | 136 +++++++++++++++--- 2 files changed, 135 insertions(+), 19 deletions(-) diff --git a/docs/plugins/docusaurus-plugin-ai-docs/README.md b/docs/plugins/docusaurus-plugin-ai-docs/README.md index b7b0226..7112073 100644 --- a/docs/plugins/docusaurus-plugin-ai-docs/README.md +++ b/docs/plugins/docusaurus-plugin-ai-docs/README.md @@ -10,6 +10,9 @@ This plugin implements the "Third Audience" pattern for AI agents and LLM crawle 2. **Auto-Discovery**: Injects `` tags in every HTML page's `` pointing to the corresponding Markdown file. - Example: `` +3. **Search Index**: Generates a static JSON index of the documentation for client-side or external search tooling. + - Example: `https://temba.bouwe.io/search_index.json` + ## How It Works The plugin uses two key mechanisms: @@ -19,6 +22,7 @@ During the `postBuild` lifecycle hook, the plugin: - Scans the `docs/` directory for all `.md` and `.mdx` files - Extracts document IDs from frontmatter to determine URL routes - Copies each Markdown file to the build output directory at the correct path +- Generates `search_index.json` with each page's title, URL, keywords, and Markdown content - Injects `` tags directly into the generated HTML files ### 2. URL Matching @@ -52,6 +56,7 @@ After building, your HTML pages will include meta tags like this: And the corresponding Markdown file will be accessible: - HTML: `https://temba.bouwe.io/docs/documentation` - Markdown: `https://temba.bouwe.io/docs/documentation.md` +- Search index: `https://temba.bouwe.io/search_index.json` ## Testing @@ -73,6 +78,9 @@ curl http://localhost:4444/docs/api/functions/create.md # Test meta tag injection curl http://localhost:4444/docs/getting-started.html | grep "text/markdown" curl http://localhost:4444/docs/api/functions/create.html | grep "text/markdown" + +# Test search index generation +curl http://localhost:4444/search_index.json ``` Expected output: @@ -88,6 +96,16 @@ title: Documentation # Meta tags + +# Search index +[ + { + "title": "Getting Started", + "url": "/docs/getting-started", + "keywords": [], + "content": "# Getting Started\n\nPrerequisites you need to have:\n..." + } +] ``` ## Benefits diff --git a/docs/plugins/docusaurus-plugin-ai-docs/index.js b/docs/plugins/docusaurus-plugin-ai-docs/index.js index 1405012..333c6c4 100644 --- a/docs/plugins/docusaurus-plugin-ai-docs/index.js +++ b/docs/plugins/docusaurus-plugin-ai-docs/index.js @@ -1,6 +1,103 @@ const fs = require('fs-extra'); const path = require('path'); -const glob = require('glob'); + +async function findMarkdownFiles(directory, rootDirectory = directory) { + const entries = await fs.readdir(directory, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + const entryPath = path.join(directory, entry.name); + + if (entry.isDirectory()) { + files.push(...await findMarkdownFiles(entryPath, rootDirectory)); + continue; + } + + if (entry.isFile() && /\.(md|mdx)$/.test(entry.name)) { + files.push(path.relative(rootDirectory, entryPath)); + } + } + + return files.sort(); +} + +function parseFrontmatter(content) { + const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/); + + if (!match) { + return { data: {}, body: content }; + } + + const data = {}; + const frontmatter = match[1]; + + for (const line of frontmatter.split('\n')) { + const fieldMatch = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/); + + if (fieldMatch) { + data[fieldMatch[1]] = fieldMatch[2].trim(); + } + } + + return { + data, + body: content.slice(match[0].length), + }; +} + +function stripQuotes(value) { + return value.replace(/^['"]|['"]$/g, ''); +} + +function parseKeywords(value) { + if (!value) { + return []; + } + + const trimmedValue = value.trim(); + + if (trimmedValue.startsWith('[') && trimmedValue.endsWith(']')) { + return trimmedValue + .slice(1, -1) + .split(',') + .map(keyword => stripQuotes(keyword.trim())) + .filter(Boolean); + } + + return [stripQuotes(trimmedValue)].filter(Boolean); +} + +function deriveTitle(file, body, frontmatterTitle) { + if (frontmatterTitle) { + return stripQuotes(frontmatterTitle); + } + + const headingMatch = body.match(/^#\s+(.+)$/m); + + if (headingMatch) { + return headingMatch[1].trim(); + } + + const basename = path.basename(file, path.extname(file)); + + return basename + .split(/[-_]/) + .filter(Boolean) + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +} + +function resolveRoute(file, docId, routeMap) { + const route = routeMap[docId]; + + if (route) { + return route; + } + + const relativePath = file.replace(/\.(md|mdx)$/, '').replace(/\/index$/, ''); + + return `/docs/${relativePath}`; +} /** * Docusaurus Plugin: AI Documentation @@ -8,6 +105,7 @@ const glob = require('glob'); * This plugin implements the "Third Audience" pattern for AI agents: * 1. Copies markdown files to build output directory matching URL structure * 2. Injects meta tags in HTML to make markdown discoverable + * 3. Generates a static search_index.json for the full documentation */ module.exports = function (context, options) { return { @@ -21,12 +119,8 @@ module.exports = function (context, options) { const docsPath = path.join(context.siteDir, 'docs'); - // Find all markdown files in the docs directory - const markdownFiles = glob.sync('**/*.{md,mdx}', { - cwd: docsPath, - absolute: false, - ignore: ['api/index.md'], - }); + const markdownFiles = (await findMarkdownFiles(docsPath)) + .filter(file => file !== 'api/index.md'); console.log(`[AI Docs Plugin] Found ${markdownFiles.length} markdown files`); @@ -44,6 +138,7 @@ module.exports = function (context, options) { // Map to store doc routes for HTML injection const docRoutes = []; + const searchIndex = []; // Copy each markdown file to match its URL structure for (const file of markdownFiles) { @@ -51,19 +146,10 @@ module.exports = function (context, options) { try { const content = await fs.readFile(sourcePath, 'utf-8'); + const { data: frontmatter, body } = parseFrontmatter(content); - // Extract the doc ID from frontmatter - const idMatch = content.match(/^---\s*\n[\s\S]*?id:\s*(.+?)\s*\n[\s\S]*?---/m); - const docId = idMatch ? idMatch[1].trim() : path.basename(file, path.extname(file)); - - // Determine the route - let route = routeMap[docId]; - - if (!route) { - // Fallback: construct from file path - const relativePath = file.replace(/\.(md|mdx)$/, '').replace(/\/index$/, ''); - route = `/docs/${relativePath}`; - } + const docId = frontmatter.id || path.basename(file, path.extname(file)); + const route = resolveRoute(file, docId, routeMap); // Remove leading slash and create the destination path const routePath = route.replace(/^\//, ''); @@ -78,11 +164,23 @@ module.exports = function (context, options) { // Store route for HTML injection docRoutes.push({ route, routePath }); + searchIndex.push({ + title: deriveTitle(file, body, frontmatter.title), + url: route, + keywords: parseKeywords(frontmatter.keywords || frontmatter.tags), + content: body.trim(), + }); } catch (error) { console.error(`[AI Docs Plugin] Error processing ${file}:`, error.message); } } + searchIndex.sort((first, second) => first.url.localeCompare(second.url)); + + const searchIndexPath = path.join(outDir, 'search_index.json'); + await fs.writeFile(searchIndexPath, `${JSON.stringify(searchIndex, null, 2)}\n`, 'utf-8'); + console.log(`[AI Docs Plugin] Generated search index: search_index.json (${searchIndex.length} entries)`); + // Now inject meta tags into HTML files console.log('[AI Docs Plugin] Injecting meta tags into HTML files...'); From 8ba22826d8d6e417deca809f30659333692e1027 Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Thu, 4 Jun 2026 16:52:42 +0200 Subject: [PATCH 02/33] feat: MCP scaffolding --- package-lock.json | 13 +++++++- packages/mcp/README.md | 3 ++ packages/mcp/cli.js | 18 +++++++++++ packages/mcp/mcp.js | 1 + packages/mcp/nus.config.js | 5 +++ packages/mcp/package.json | 17 +++++++++++ publish-mcp.sh | 62 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 packages/mcp/README.md create mode 100644 packages/mcp/cli.js create mode 100644 packages/mcp/mcp.js create mode 100644 packages/mcp/nus.config.js create mode 100644 packages/mcp/package.json create mode 100755 publish-mcp.sh diff --git a/package-lock.json b/package-lock.json index 64dbd1c..b5e7136 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ } }, "docs": { - "version": "0.70.0", + "version": "0.71.0", "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/preset-classic": "3.9.2", @@ -16602,6 +16602,10 @@ "resolved": "packages/cli", "link": true }, + "node_modules/temba-mcp": { + "resolved": "packages/mcp", + "link": true + }, "node_modules/terser": { "version": "5.46.0", "license": "BSD-2-Clause", @@ -18269,6 +18273,13 @@ "temba": "cli.js" } }, + "packages/mcp": { + "name": "temba-mcp", + "version": "0.1.0", + "bin": { + "temba-mcp": "cli.js" + } + }, "packages/temba": { "version": "0.71.0", "license": "ISC", diff --git a/packages/mcp/README.md b/packages/mcp/README.md new file mode 100644 index 0000000..d3e204f --- /dev/null +++ b/packages/mcp/README.md @@ -0,0 +1,3 @@ +# Temba Docs MCP + +You can ensure your AI tools have current Temba knowledge through the Temba Docs MCP (Model Context Protocol) server. This provides real-time access to the latest documentation, helping AI tools avoid outdated recommendations and ensuring they understand current best practices. diff --git a/packages/mcp/cli.js b/packages/mcp/cli.js new file mode 100644 index 0000000..c4ecc68 --- /dev/null +++ b/packages/mcp/cli.js @@ -0,0 +1,18 @@ +#!/usr/bin/env node + +import { go } from './mcp.js' + +const [, , command, ...args] = process.argv + +const ensure = (condition = false, message) => { + if (!condition) { + console.error(message) + process.exit(1) + } +} + +console.log('\n✨ Temba Docs MCP') + +go() + +console.log('') diff --git a/packages/mcp/mcp.js b/packages/mcp/mcp.js new file mode 100644 index 0000000..522b528 --- /dev/null +++ b/packages/mcp/mcp.js @@ -0,0 +1 @@ +export const go = () => console.log('MCP is not implemented yet. Stay tuned!') diff --git a/packages/mcp/nus.config.js b/packages/mcp/nus.config.js new file mode 100644 index 0000000..e310659 --- /dev/null +++ b/packages/mcp/nus.config.js @@ -0,0 +1,5 @@ +export default { + minAge: 1440, + tool: 'npm', + overrides: {}, +} diff --git a/packages/mcp/package.json b/packages/mcp/package.json new file mode 100644 index 0000000..8351705 --- /dev/null +++ b/packages/mcp/package.json @@ -0,0 +1,17 @@ +{ + "name": "temba-mcp", + "version": "0.0.1", + "description": "MCP for Temba documentation", + "author": "Bouwe (https://bouwe.io)", + "scripts": { + "update": "npx -y jelmerro/nus" + }, + "bin": { + "temba-mcp": "./cli.js" + }, + "type": "module", + "files": [ + "cli.js", + "mcp.js" + ] +} diff --git a/publish-mcp.sh b/publish-mcp.sh new file mode 100755 index 0000000..f305287 --- /dev/null +++ b/publish-mcp.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +TYPE=$1 +DRY_RUN=false + +if [[ " $* " =~ " --dry-run " ]]; then + DRY_RUN=true + echo "DRY RUN MODE ENABLED" +fi + +if [ "$TYPE" != "major" ] && [ "$TYPE" != "minor" ] && [ "$TYPE" != "patch" ]; then + echo "Usage: ./publish-mcp.sh [major|minor|patch] [--dry-run]" + exit 1 +fi + +if [ "$DRY_RUN" = false ] && [ -n "$(git status --porcelain)" ]; then + echo "Error: Commit all changes before publishing" + exit 1 +fi + +if [ "$DRY_RUN" = false ]; then + echo "Checking NPM login status..." + if npm whoami &> /dev/null; then + echo "Logged in as $(npm whoami)" + else + echo "Not logged in to NPM." + npm login + + if [ $? -ne 0 ]; then + echo "Login failed or was cancelled. Exiting." + exit 1 + fi + fi +else + echo "[DRY RUN] Would check NPM login status" +fi + +CURRENT_VERSION=$(node -p "require('./packages/mcp/package.json').version") +NEXT_VERSION=$(node -p "const [ma, mi, pa] = '$CURRENT_VERSION'.split('.').map(Number); '$TYPE' === 'major' ? \`\${ma+1}.0.0\` : '$TYPE' === 'minor' ? \`\${ma}.\${mi+1}.0\` : \`\${ma}.\${mi}.\${pa+1}\`") + +echo "Releasing temba-mcp $NEXT_VERSION (from $CURRENT_VERSION)..." + +run_cmd() { + if [ "$DRY_RUN" = true ]; then + echo "[DRY RUN] Would execute: $*" + else + "$@" + fi +} + +run_cmd npm version "$TYPE" -w packages/mcp --no-git-tag-version +run_cmd npm publish -w packages/mcp + +echo "Finalizing Git..." +run_cmd git add packages/mcp/package.json package-lock.json +run_cmd git commit -m "temba-mcp $NEXT_VERSION" + +if [ "$DRY_RUN" = false ]; then + echo "Done. temba-mcp $NEXT_VERSION published." +else + echo "Dry run complete. No changes were made." +fi From f363c3ac2477cbc8ac04ebaab9a852e9a43a1b1f Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Thu, 4 Jun 2026 16:55:48 +0200 Subject: [PATCH 03/33] chore: reset version number --- packages/mcp/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 8351705..7887a55 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "temba-mcp", - "version": "0.0.1", + "version": "0.0.0", "description": "MCP for Temba documentation", "author": "Bouwe (https://bouwe.io)", "scripts": { From d5cfb0153790f4044ba363b354c496b9bb0b4601 Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Thu, 4 Jun 2026 16:56:14 +0200 Subject: [PATCH 04/33] temba-mcp 0.0.1 --- package-lock.json | 2 +- packages/mcp/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index b5e7136..e07a92e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18275,7 +18275,7 @@ }, "packages/mcp": { "name": "temba-mcp", - "version": "0.1.0", + "version": "0.0.1", "bin": { "temba-mcp": "cli.js" } diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 7887a55..8351705 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "temba-mcp", - "version": "0.0.0", + "version": "0.0.1", "description": "MCP for Temba documentation", "author": "Bouwe (https://bouwe.io)", "scripts": { From 6b7d4d4569d88bed680f88c97b9d67bb706f797f Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Thu, 4 Jun 2026 16:58:43 +0200 Subject: [PATCH 05/33] chore: add MCP to repo docs --- devguide.md | 28 ++++++++++++++++++++++++++-- packages/mcp/cli.js | 0 2 files changed, 26 insertions(+), 2 deletions(-) mode change 100644 => 100755 packages/mcp/cli.js diff --git a/devguide.md b/devguide.md index 7b85eb7..2f98b64 100644 --- a/devguide.md +++ b/devguide.md @@ -2,7 +2,7 @@ Notes to self and contributors on how to develop and release Temba. -This repo is a monorepo containing the workspaces `packages/cli`, `packages/temba`, `docs`, and `examples`. +This repo is a monorepo containing the workspaces `packages/cli`, `packages/temba`, `packages/mcp`, `docs`, and `examples`. > [!IMPORTANT] > As this is a monorepo, all commands are always called from the monorepo root folder, @@ -113,4 +113,28 @@ Write your release notes. Commit and push the remaining changes in your feature branch. -Merge the PR to `main`. \ No newline at end of file +Merge the PR to `main`. + +## Publishing the MCP package + +The MCP package is versioned independently from Temba, the CLI, examples, and docs. Do not include it in the shared `./publish.sh` release flow. + +To publish a new MCP version from the root folder: + +```bash +./publish-mcp.sh [patch|minor|major] +``` + +Use `--dry-run` to inspect the release steps without changing the version or publishing: + +```bash +./publish-mcp.sh patch --dry-run +``` + +The script bumps only `packages/mcp/package.json`, publishes only the `packages/mcp` workspace, then commits the MCP package version and lockfile changes. + +For the first npm publish of an already prepared version, publish the workspace directly instead of bumping again: + +```bash +npm publish -w packages/mcp +``` diff --git a/packages/mcp/cli.js b/packages/mcp/cli.js old mode 100644 new mode 100755 From 176ce3073b2ad522a0545fa8443213c560a2c10d Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Thu, 4 Jun 2026 17:24:59 +0200 Subject: [PATCH 06/33] chore: add versioning --- devguide.md | 2 +- packages/mcp/version.js | 1 + publish-mcp.sh | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 packages/mcp/version.js diff --git a/devguide.md b/devguide.md index 2f98b64..12e2fae 100644 --- a/devguide.md +++ b/devguide.md @@ -131,7 +131,7 @@ Use `--dry-run` to inspect the release steps without changing the version or pub ./publish-mcp.sh patch --dry-run ``` -The script bumps only `packages/mcp/package.json`, publishes only the `packages/mcp` workspace, then commits the MCP package version and lockfile changes. +The script bumps only `packages/mcp/package.json`, updates `packages/mcp/version.js`, publishes only the `packages/mcp` workspace, then commits the MCP package version and lockfile changes. For the first npm publish of an already prepared version, publish the workspace directly instead of bumping again: diff --git a/packages/mcp/version.js b/packages/mcp/version.js new file mode 100644 index 0000000..6af1fbc --- /dev/null +++ b/packages/mcp/version.js @@ -0,0 +1 @@ +export const version = '0.0.1' diff --git a/publish-mcp.sh b/publish-mcp.sh index f305287..6c820bc 100755 --- a/publish-mcp.sh +++ b/publish-mcp.sh @@ -49,10 +49,11 @@ run_cmd() { } run_cmd npm version "$TYPE" -w packages/mcp --no-git-tag-version +run_cmd bash -c "echo \"export const version = '$NEXT_VERSION'\" > packages/mcp/version.js" run_cmd npm publish -w packages/mcp echo "Finalizing Git..." -run_cmd git add packages/mcp/package.json package-lock.json +run_cmd git add packages/mcp/package.json packages/mcp/version.js package-lock.json run_cmd git commit -m "temba-mcp $NEXT_VERSION" if [ "$DRY_RUN" = false ]; then From 6cf83f6030587f43dbcde01143a958210eaee651 Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Thu, 4 Jun 2026 17:25:43 +0200 Subject: [PATCH 07/33] feat: implement MCP --- package-lock.json | 73 ++++++++++++++++++++++++++++++++++++++ packages/mcp/cli.js | 18 ++-------- packages/mcp/mcp.js | 49 ++++++++++++++++++++++++- packages/mcp/package.json | 10 ++++-- packages/mcp/searchDocs.js | 9 +++++ 5 files changed, 141 insertions(+), 18 deletions(-) create mode 100644 packages/mcp/searchDocs.js diff --git a/package-lock.json b/package-lock.json index e07a92e..b0fc2f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4428,6 +4428,66 @@ "react": ">=16" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.4.0.tgz", + "integrity": "sha512-79gx8xh4o9YzdbtqMukOe5WKzvEZpvBA1x8PAgJWL7J5k06+vJx8NK2kWzOazPgqnfDego7cNEO8tjai/nOPAA==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "raw-body": "^3.0.0", + "zod": "^3.23.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@noble/hashes": { "version": "1.8.0", "dev": true, @@ -18255,6 +18315,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zwitch": { "version": "2.0.4", "license": "MIT", @@ -18276,6 +18345,10 @@ "packages/mcp": { "name": "temba-mcp", "version": "0.0.1", + "dependencies": { + "@modelcontextprotocol/sdk": "0.4.0", + "zod": "4.4.3" + }, "bin": { "temba-mcp": "cli.js" } diff --git a/packages/mcp/cli.js b/packages/mcp/cli.js index c4ecc68..67ce5f7 100755 --- a/packages/mcp/cli.js +++ b/packages/mcp/cli.js @@ -1,18 +1,6 @@ #!/usr/bin/env node +import { startMcpServer } from './mcp.js' -import { go } from './mcp.js' +console.error('✨ Temba Docs MCP starting...') -const [, , command, ...args] = process.argv - -const ensure = (condition = false, message) => { - if (!condition) { - console.error(message) - process.exit(1) - } -} - -console.log('\n✨ Temba Docs MCP') - -go() - -console.log('') +startMcpServer().catch(console.error) diff --git a/packages/mcp/mcp.js b/packages/mcp/mcp.js index 522b528..54e6b4f 100644 --- a/packages/mcp/mcp.js +++ b/packages/mcp/mcp.js @@ -1 +1,48 @@ -export const go = () => console.log('MCP is not implemented yet. Stay tuned!') +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' +import { z } from 'zod' +import { searchDocs } from './searchDocs.js' +import { version } from './version.js' + +export const startMcpServer = async () => { + const server = new McpServer({ + name: 'temba-docs-mcp', + version, + }) + + // Fetch the index once on startup + let index = [] + const searchIndexUrl = 'https://docs.temba.io/search-index.json' + try { + const response = await fetch(searchIndexUrl) + index = await response.json() + } catch (e) { + console.error('Failed to fetch ' + searchIndexUrl, e) + } + + // Register the tool + server.tool( + 'search_docs', + 'Search the library documentation', + { query: z.string() }, + async ({ query }) => { + const results = searchDocs(query, index).slice(0, 5) // Limit to top 5 results + + if (results.length === 0) { + return { + content: [{ type: 'text', text: 'No documentation found for your query.' }], + } + } + + return { + content: results.map((page) => ({ + type: 'text', + text: `Title: ${page.title}\nURL: ${page.url}\n\nContent:\n${page.content}`, + })), + } + }, + ) + + const transport = new StdioServerTransport() + await server.connect(transport) +} diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 8351705..689469f 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -12,6 +12,12 @@ "type": "module", "files": [ "cli.js", - "mcp.js" - ] + "mcp.js", + "searchDocs.js", + "version.js" + ], + "dependencies": { + "@modelcontextprotocol/sdk": "0.4.0", + "zod": "4.4.3" + } } diff --git a/packages/mcp/searchDocs.js b/packages/mcp/searchDocs.js new file mode 100644 index 0000000..59b7bfd --- /dev/null +++ b/packages/mcp/searchDocs.js @@ -0,0 +1,9 @@ +export const searchDocs = (query, index) => { + const lowerQuery = query.toLowerCase() + return index.filter( + (page) => + page.content.toLowerCase().includes(lowerQuery) || + page.title.toLowerCase().includes(lowerQuery) || + (page.keywords && page.keywords.some((k) => k.toLowerCase().includes(lowerQuery))), + ) +} From 10c7e049fcfc1e2168bed750db0d537a3b16734f Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Thu, 4 Jun 2026 17:29:27 +0200 Subject: [PATCH 08/33] temba-mcp 0.1.0 --- package-lock.json | 2 +- packages/mcp/package.json | 2 +- packages/mcp/version.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b0fc2f4..b3562f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18344,7 +18344,7 @@ }, "packages/mcp": { "name": "temba-mcp", - "version": "0.0.1", + "version": "0.1.0", "dependencies": { "@modelcontextprotocol/sdk": "0.4.0", "zod": "4.4.3" diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 689469f..a69fdac 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "temba-mcp", - "version": "0.0.1", + "version": "0.1.0", "description": "MCP for Temba documentation", "author": "Bouwe (https://bouwe.io)", "scripts": { diff --git a/packages/mcp/version.js b/packages/mcp/version.js index 6af1fbc..6ccd2f1 100644 --- a/packages/mcp/version.js +++ b/packages/mcp/version.js @@ -1 +1 @@ -export const version = '0.0.1' +export const version = '0.1.0' From dccfa73be5840e3ac16569b8fdaa6fb61bbf8c8b Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Thu, 4 Jun 2026 17:35:46 +0200 Subject: [PATCH 09/33] fix: MCP SDK dependency --- package-lock.json | 486 +++++++++++++++++++++++++++++++++++++- packages/mcp/package.json | 2 +- 2 files changed, 474 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index b3562f6..b4c0622 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3895,6 +3895,18 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "dev": true, @@ -4429,14 +4441,97 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.4.0.tgz", - "integrity": "sha512-79gx8xh4o9YzdbtqMukOe5WKzvEZpvBA1x8PAgJWL7J5k06+vJx8NK2kWzOazPgqnfDego7cNEO8tjai/nOPAA==", + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "license": "MIT", "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8" + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/@modelcontextprotocol/sdk/node_modules/bytes": { @@ -4448,6 +4543,92 @@ "node": ">= 0.8" } }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -4464,6 +4645,82 @@ "url": "https://opencollective.com/express" } }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", @@ -4479,13 +4736,80 @@ "node": ">= 0.10" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, "funding": { - "url": "https://github.com/sponsors/colinhacks" + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/@noble/hashes": { @@ -7016,7 +7340,6 @@ }, "node_modules/cookie-signature": { "version": "1.2.2", - "dev": true, "license": "MIT", "engines": { "node": ">=6.6.0" @@ -7126,6 +7449,23 @@ "version": "1.0.3", "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cosmiconfig": { "version": "8.3.6", "license": "MIT", @@ -8499,6 +8839,27 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "license": "MIT", @@ -8572,6 +8933,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/express/node_modules/content-disposition": { "version": "0.5.4", "license": "MIT", @@ -9550,6 +9929,15 @@ "react-is": "^16.7.0" } }, + "node_modules/hono": { + "version": "4.12.23", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", + "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/hpack.js": { "version": "2.1.6", "license": "MIT", @@ -9889,6 +10277,15 @@ "loose-envify": "^1.0.0" } }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "2.3.0", "license": "MIT", @@ -10117,6 +10514,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-regexp": { "version": "1.0.0", "license": "MIT", @@ -10229,6 +10632,15 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT" @@ -10265,6 +10677,12 @@ "version": "1.0.0", "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "dev": true, @@ -13006,7 +13424,6 @@ }, "node_modules/once": { "version": "1.4.0", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -13383,6 +13800,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-dir": { "version": "7.0.0", "license": "MIT", @@ -15722,6 +16148,32 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/rtlcss": { "version": "4.3.0", "license": "MIT", @@ -18201,7 +18653,6 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { @@ -18324,6 +18775,15 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, "node_modules/zwitch": { "version": "2.0.4", "license": "MIT", @@ -18346,7 +18806,7 @@ "name": "temba-mcp", "version": "0.1.0", "dependencies": { - "@modelcontextprotocol/sdk": "0.4.0", + "@modelcontextprotocol/sdk": "^1.29.0", "zod": "4.4.3" }, "bin": { diff --git a/packages/mcp/package.json b/packages/mcp/package.json index a69fdac..3bf7c8a 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -17,7 +17,7 @@ "version.js" ], "dependencies": { - "@modelcontextprotocol/sdk": "0.4.0", + "@modelcontextprotocol/sdk": "^1.29.0", "zod": "4.4.3" } } From 61a6f20ab5af279b7098fc870e63c87a4ae0010a Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Thu, 4 Jun 2026 17:36:09 +0200 Subject: [PATCH 10/33] temba-mcp 0.1.1 --- package-lock.json | 2 +- packages/mcp/package.json | 2 +- packages/mcp/version.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b4c0622..2335e79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18804,7 +18804,7 @@ }, "packages/mcp": { "name": "temba-mcp", - "version": "0.1.0", + "version": "0.1.1", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "zod": "4.4.3" diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 3bf7c8a..79eded3 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "temba-mcp", - "version": "0.1.0", + "version": "0.1.1", "description": "MCP for Temba documentation", "author": "Bouwe (https://bouwe.io)", "scripts": { diff --git a/packages/mcp/version.js b/packages/mcp/version.js index 6ccd2f1..8c0f2de 100644 --- a/packages/mcp/version.js +++ b/packages/mcp/version.js @@ -1 +1 @@ -export const version = '0.1.0' +export const version = '0.1.1' From df107e7452381aad608561cfa32abd7de593ea29 Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Thu, 4 Jun 2026 17:37:51 +0200 Subject: [PATCH 11/33] fix: docs URL --- packages/mcp/mcp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp/mcp.js b/packages/mcp/mcp.js index 54e6b4f..7d75bc3 100644 --- a/packages/mcp/mcp.js +++ b/packages/mcp/mcp.js @@ -12,7 +12,7 @@ export const startMcpServer = async () => { // Fetch the index once on startup let index = [] - const searchIndexUrl = 'https://docs.temba.io/search-index.json' + const searchIndexUrl = 'https://temba.bouwe.io/search-index.json' try { const response = await fetch(searchIndexUrl) index = await response.json() From f5e7f34a5e515bca441b30cb9819a971aac1a1e2 Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Thu, 4 Jun 2026 17:37:56 +0200 Subject: [PATCH 12/33] temba-mcp 0.1.2 --- package-lock.json | 2 +- packages/mcp/package.json | 2 +- packages/mcp/version.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2335e79..39b6d83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18804,7 +18804,7 @@ }, "packages/mcp": { "name": "temba-mcp", - "version": "0.1.1", + "version": "0.1.2", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "zod": "4.4.3" diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 79eded3..da00fbe 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "temba-mcp", - "version": "0.1.1", + "version": "0.1.2", "description": "MCP for Temba documentation", "author": "Bouwe (https://bouwe.io)", "scripts": { diff --git a/packages/mcp/version.js b/packages/mcp/version.js index 8c0f2de..87158aa 100644 --- a/packages/mcp/version.js +++ b/packages/mcp/version.js @@ -1 +1 @@ -export const version = '0.1.1' +export const version = '0.1.2' From 77c8c3c310e3e87e79a6a36efabef76d6f73dfac Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Thu, 4 Jun 2026 17:41:33 +0200 Subject: [PATCH 13/33] fix: docs URL again, and handle URL not found better --- packages/mcp/mcp.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/mcp/mcp.js b/packages/mcp/mcp.js index 7d75bc3..c366aab 100644 --- a/packages/mcp/mcp.js +++ b/packages/mcp/mcp.js @@ -12,9 +12,18 @@ export const startMcpServer = async () => { // Fetch the index once on startup let index = [] - const searchIndexUrl = 'https://temba.bouwe.io/search-index.json' + const searchIndexUrl = 'https://temba.bouwe.io/search_index.json' try { const response = await fetch(searchIndexUrl) + if (!response.ok) { + throw new Error(`HTTP ${response.status} ${response.statusText}`) + } + + const contentType = response.headers.get('content-type') || '' + if (!contentType.includes('application/json')) { + throw new Error(`Expected JSON, received ${contentType || 'unknown content type'}`) + } + index = await response.json() } catch (e) { console.error('Failed to fetch ' + searchIndexUrl, e) From 17cc0e6dc644ce64999172051579d19884669be8 Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Thu, 4 Jun 2026 17:42:14 +0200 Subject: [PATCH 14/33] temba-mcp 0.1.3 --- package-lock.json | 2 +- packages/mcp/package.json | 2 +- packages/mcp/version.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 39b6d83..efc4b18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18804,7 +18804,7 @@ }, "packages/mcp": { "name": "temba-mcp", - "version": "0.1.2", + "version": "0.1.3", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "zod": "4.4.3" diff --git a/packages/mcp/package.json b/packages/mcp/package.json index da00fbe..559bc90 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "temba-mcp", - "version": "0.1.2", + "version": "0.1.3", "description": "MCP for Temba documentation", "author": "Bouwe (https://bouwe.io)", "scripts": { diff --git a/packages/mcp/version.js b/packages/mcp/version.js index 87158aa..d249a39 100644 --- a/packages/mcp/version.js +++ b/packages/mcp/version.js @@ -1 +1 @@ -export const version = '0.1.2' +export const version = '0.1.3' From 55bf4056cc1abadacf710157ab72592ba00db116 Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Thu, 4 Jun 2026 21:08:11 +0200 Subject: [PATCH 15/33] docs: Temba Docs MCP --- docs/docs/getting-started.md | 4 ++++ docs/docs/mcp.md | 35 +++++++++++++++++++++++++++++++++++ docs/sidebars.ts | 1 + 3 files changed, 40 insertions(+) create mode 100644 docs/docs/mcp.md diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md index ed0bffd..7c2f2b4 100644 --- a/docs/docs/getting-started.md +++ b/docs/docs/getting-started.md @@ -64,6 +64,10 @@ server.start() ✅ Server listening on port 8362 ``` +### Temba Docs MCP + +If you use AI tools while building with Temba, point them at the [Temba Docs MCP](/docs/mcp) so they can use the latest documentation and current best practices. + ### Configuration To opt-out or customize Temba's workings, pass a `config` object to the `create` function. Check out the individual feature pages in the sidebar, or the [config settings overview](/docs/overview#config-settings-overview). diff --git a/docs/docs/mcp.md b/docs/docs/mcp.md new file mode 100644 index 0000000..c4e9eea --- /dev/null +++ b/docs/docs/mcp.md @@ -0,0 +1,35 @@ +--- +id: mcp +title: Temba Docs MCP +sidebar_position: 2 +--- + +# Temba Docs MCP + +You can ensure your AI tools have current Temba knowledge through the Temba Docs MCP (Model Context Protocol) server. This provides real-time access to the latest documentation, helping AI tools avoid outdated recommendations and ensuring they understand current best practices. + +Unlike AI models trained on static data, the MCP server provides access to the latest Temba documentation. The server is free and open-source. + +# Installation + +The setup process varies depending on your AI development tool. You may see some tools refer to MCP servers as connectors, adapters, extensions, or plugins. + +- [ChatGPT](https://platform.openai.com/docs/mcp#test-and-connect-your-mcp-server) +- [Claude.ai / Claude Desktop](https://support.anthropic.com/en/articles/10168395-setting-up-integrations-on-claude-ai#h_cda40ecb32) +- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code/mcp) +- [Claude Code GitHub Action](https://github.com/anthropics/claude-code-action?tab=readme-ov-file#using-custom-mcp-configuration) +- [Codex CLI](https://developers.openai.com/codex/mcp) +- [Cursor](https://docs.cursor.com/context/mcp) +- [Gemini CLI](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md) +- [GitHub Copilot Coding Agent](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/extend-coding-agent-with-mcp) +- [Google Antigravity](https://antigravity.google/) +- [Opencode AI](https://opencode.ai/) +- [Raycast](https://manual.raycast.com/model-context-protocol) +- [Visual Studio Code](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server) +- [Warp](https://docs.warp.dev/knowledge-and-collaboration/mcp) +- [Windsurf](https://docs.windsurf.com/windsurf/cascade/mcp#mcp-config-json) +- [Zed](https://zed.dev/docs/ai/mcp) + +### Usage + +Once configured, you can ask your AI tool questions about Temba, and it will retrieve information directly from the latest docs. Coding agents will be able to consult the latest documentation when performing coding tasks, and chatbots will be able to accurately answer questions about Temba features, APIs, and best practices. diff --git a/docs/sidebars.ts b/docs/sidebars.ts index fea183f..7d5924c 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -6,6 +6,7 @@ const typedocSidebar = require('./docs/api/typedoc-sidebar.cjs') const sidebars: SidebarsConfig = { tembaSidebar: [ 'getting-started', + 'mcp', 'overview', 'examples', { From 8359071dd828ef7569f3ec599c96c52920f234db Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Thu, 4 Jun 2026 22:38:35 +0200 Subject: [PATCH 16/33] chore: for mcp move implementation to src folder, add tests --- devguide.md | 8 ++- package-lock.json | 5 +- package.json | 2 +- packages/mcp/package.json | 12 ++-- packages/mcp/searchDocs.js | 9 --- packages/mcp/{ => src}/cli.js | 0 packages/mcp/{ => src}/mcp.js | 0 packages/mcp/src/searchDocs.js | 16 +++++ packages/mcp/{ => src}/version.js | 0 packages/mcp/test/searchDocs.test.js | 87 ++++++++++++++++++++++++++++ 10 files changed, 122 insertions(+), 17 deletions(-) delete mode 100644 packages/mcp/searchDocs.js rename packages/mcp/{ => src}/cli.js (100%) rename packages/mcp/{ => src}/mcp.js (100%) create mode 100644 packages/mcp/src/searchDocs.js rename packages/mcp/{ => src}/version.js (100%) create mode 100644 packages/mcp/test/searchDocs.test.js diff --git a/devguide.md b/devguide.md index 12e2fae..56b062c 100644 --- a/devguide.md +++ b/devguide.md @@ -13,7 +13,7 @@ This repo is a monorepo containing the workspaces `packages/cli`, `packages/temb You can run these commands directly from the root: ```bash -npm test # Runs tests for the Temba library +npm test # Runs tests for the Temba library and MCP package npm run lint # Runs linting for the Temba library ``` @@ -23,6 +23,12 @@ Or combine them in one go: npm run check ``` +To run only the MCP package tests: + +```bash +npm test -w packages/mcp +``` + ## MongoDB E2E testing To also run the integration tests against a real MongoDB, you need a local MongoDB diff --git a/package-lock.json b/package-lock.json index efc4b18..09c5e14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18810,7 +18810,10 @@ "zod": "4.4.3" }, "bin": { - "temba-mcp": "cli.js" + "temba-mcp": "src/cli.js" + }, + "devDependencies": { + "vitest": "^4.0.18" } }, "packages/temba": { diff --git a/package.json b/package.json index dd6e5b5..7fa6c8c 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "docs" ], "scripts": { - "test": "npm test -w packages/temba", + "test": "npm run test --workspaces --if-present", "test:mongodb": "npm run test:mongodb -w packages/temba", "test:watch": "npm run test:watch -w packages/temba", "lint": "npm run lint -w packages/temba", diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 559bc90..9c9c4a6 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -4,20 +4,22 @@ "description": "MCP for Temba documentation", "author": "Bouwe (https://bouwe.io)", "scripts": { + "test": "vitest run", + "test:watch": "vitest --watch", "update": "npx -y jelmerro/nus" }, "bin": { - "temba-mcp": "./cli.js" + "temba-mcp": "./src/cli.js" }, "type": "module", "files": [ - "cli.js", - "mcp.js", - "searchDocs.js", - "version.js" + "src" ], "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "zod": "4.4.3" + }, + "devDependencies": { + "vitest": "^4.0.18" } } diff --git a/packages/mcp/searchDocs.js b/packages/mcp/searchDocs.js deleted file mode 100644 index 59b7bfd..0000000 --- a/packages/mcp/searchDocs.js +++ /dev/null @@ -1,9 +0,0 @@ -export const searchDocs = (query, index) => { - const lowerQuery = query.toLowerCase() - return index.filter( - (page) => - page.content.toLowerCase().includes(lowerQuery) || - page.title.toLowerCase().includes(lowerQuery) || - (page.keywords && page.keywords.some((k) => k.toLowerCase().includes(lowerQuery))), - ) -} diff --git a/packages/mcp/cli.js b/packages/mcp/src/cli.js similarity index 100% rename from packages/mcp/cli.js rename to packages/mcp/src/cli.js diff --git a/packages/mcp/mcp.js b/packages/mcp/src/mcp.js similarity index 100% rename from packages/mcp/mcp.js rename to packages/mcp/src/mcp.js diff --git a/packages/mcp/src/searchDocs.js b/packages/mcp/src/searchDocs.js new file mode 100644 index 0000000..228d9bc --- /dev/null +++ b/packages/mcp/src/searchDocs.js @@ -0,0 +1,16 @@ +export const searchDocs = (query, index) => { + const lowerQuery = query?.toLowerCase().trim() + + if (!lowerQuery) { + return [] + } + + return ( + index?.filter( + (page) => + page.content.toLowerCase().includes(lowerQuery) || + page.title.toLowerCase().includes(lowerQuery) || + (page.keywords && page.keywords.some((k) => k.toLowerCase().includes(lowerQuery))), + ) || [] + ) +} diff --git a/packages/mcp/version.js b/packages/mcp/src/version.js similarity index 100% rename from packages/mcp/version.js rename to packages/mcp/src/version.js diff --git a/packages/mcp/test/searchDocs.test.js b/packages/mcp/test/searchDocs.test.js new file mode 100644 index 0000000..bdc6692 --- /dev/null +++ b/packages/mcp/test/searchDocs.test.js @@ -0,0 +1,87 @@ +import { describe, expect, test } from 'vitest' +import { searchDocs } from '../src/searchDocs' + +const helloDocument = { + title: 'Hello World', + content: "Let's talk about greeting our great planet.", + keywords: ['howdy', 'earth'], +} + +const scotlandDocument = { + title: 'The most beautiful country in the world', + content: 'Scotland is so great, with its mountains, beaches and whiskies.', + keywords: ['bagpipes', 'whisky', 'howdy'], +} + +const search_index = [helloDocument, scotlandDocument] + +describe('searchDocs', () => { + describe('Finding no documents', () => { + test('Returns no results for an empty index', () => { + expect(searchDocs('anything', [])).toEqual([]) + }) + test('Returns no results for an empty query', () => { + expect(searchDocs('', search_index)).toEqual([]) + }) + test('Trims query whitespace', () => { + expect(searchDocs(' hello ', search_index)).toEqual([helloDocument]) + }) + test('Finds no results by title', async () => { + const result = searchDocs('goodbye', search_index) + expect(result).toEqual([]) + }) + test('Finds no results by content', async () => { + const result = searchDocs("Let's talk about saying goodbye to our planet.", search_index) + expect(result).toEqual([]) + }) + test('Finds no results by keyword', async () => { + const result = searchDocs('goodbye', search_index) + expect(result).toEqual([]) + }) + }) + + describe('Finding 1 document', () => { + test('Finds 1 result by title', async () => { + const result = searchDocs('hello', search_index) + expect(result).toEqual([helloDocument]) + }) + test('Finds 1 result by content', async () => { + const result = searchDocs("Let's talk about greeting our great planet.", search_index) + expect(result).toEqual([helloDocument]) + }) + test('Finds 1 result by keyword', async () => { + const result = searchDocs('earth', search_index) + expect(result).toEqual([helloDocument]) + }) + }) + + describe('Finding multiple documents', () => { + test('Finds 2 results by title', async () => { + const result = searchDocs('wOrld', search_index) + expect(result).toEqual([helloDocument, scotlandDocument]) + }) + test('Finds 2 results by content', async () => { + const result = searchDocs('great', search_index) + expect(result).toEqual([helloDocument, scotlandDocument]) + }) + test('Finds 2 results by keyword', async () => { + const result = searchDocs('howdy', search_index) + expect(result).toEqual([helloDocument, scotlandDocument]) + }) + }) + + describe('Finding partial matches', () => { + test('Finds partial matches by title', async () => { + const result = searchDocs('greet', search_index) + expect(result).toEqual([helloDocument]) + }) + test('Finds partial matches by content', async () => { + const result = searchDocs('mountain', search_index) + expect(result).toEqual([scotlandDocument]) + }) + test('Finds partial matches by keyword', async () => { + const result = searchDocs('bagpipe', search_index) + expect(result).toEqual([scotlandDocument]) + }) + }) +}) From 89b6df28786f6a8ad194d51ff1ed575ed967748b Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Fri, 5 Jun 2026 09:44:44 +0200 Subject: [PATCH 17/33] feat: auto-fetch the search_index.json every hour --- packages/mcp/src/mcp.js | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/mcp/src/mcp.js b/packages/mcp/src/mcp.js index c366aab..5b4603f 100644 --- a/packages/mcp/src/mcp.js +++ b/packages/mcp/src/mcp.js @@ -4,15 +4,14 @@ import { z } from 'zod' import { searchDocs } from './searchDocs.js' import { version } from './version.js' -export const startMcpServer = async () => { - const server = new McpServer({ - name: 'temba-docs-mcp', - version, - }) +let index = [] +let lastFetched = 0 +const CACHE_TTL = 3600000 // 1 hour in milliseconds +const searchIndexUrl = 'https://docs.temba.io/search-index.json' + +async function ensureFreshIndex() { + if (Date.now() - lastFetched < CACHE_TTL && index.length > 0) return - // Fetch the index once on startup - let index = [] - const searchIndexUrl = 'https://temba.bouwe.io/search_index.json' try { const response = await fetch(searchIndexUrl) if (!response.ok) { @@ -25,9 +24,17 @@ export const startMcpServer = async () => { } index = await response.json() + lastFetched = Date.now() } catch (e) { - console.error('Failed to fetch ' + searchIndexUrl, e) + console.error('Refresh failed, using stale index:', e) } +} + +export const startMcpServer = async () => { + const server = new McpServer({ + name: 'temba-docs-mcp', + version, + }) // Register the tool server.tool( @@ -35,6 +42,7 @@ export const startMcpServer = async () => { 'Search the library documentation', { query: z.string() }, async ({ query }) => { + await ensureFreshIndex() const results = searchDocs(query, index).slice(0, 5) // Limit to top 5 results if (results.length === 0) { From 94f9023bcc4045b0c6b8d1edf35279107beac532 Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Fri, 5 Jun 2026 09:46:44 +0200 Subject: [PATCH 18/33] temba-mcp 0.2.0 --- package-lock.json | 2 +- packages/mcp/package.json | 2 +- packages/mcp/version.js | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 packages/mcp/version.js diff --git a/package-lock.json b/package-lock.json index 09c5e14..9f395dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18804,7 +18804,7 @@ }, "packages/mcp": { "name": "temba-mcp", - "version": "0.1.3", + "version": "0.2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "zod": "4.4.3" diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 9c9c4a6..406b930 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "temba-mcp", - "version": "0.1.3", + "version": "0.2.0", "description": "MCP for Temba documentation", "author": "Bouwe (https://bouwe.io)", "scripts": { diff --git a/packages/mcp/version.js b/packages/mcp/version.js new file mode 100644 index 0000000..f11a5bd --- /dev/null +++ b/packages/mcp/version.js @@ -0,0 +1 @@ +export const version = '0.2.0' From 9b3300ec66fc6a9d7ca24875873e0200d9569e2f Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Fri, 5 Jun 2026 10:08:37 +0200 Subject: [PATCH 19/33] feat: add debug logging --- packages/mcp/src/cli.js | 9 +++++++-- packages/mcp/src/mcp.js | 14 +++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/mcp/src/cli.js b/packages/mcp/src/cli.js index 67ce5f7..9936146 100755 --- a/packages/mcp/src/cli.js +++ b/packages/mcp/src/cli.js @@ -1,6 +1,11 @@ #!/usr/bin/env node import { startMcpServer } from './mcp.js' -console.error('✨ Temba Docs MCP starting...') +const args = process.argv.slice(2) +const isDebug = args.includes('--debug') -startMcpServer().catch(console.error) +if (isDebug) { + console.error('✨ Temba Docs MCP running in DEBUG mode') +} + +startMcpServer({ debug: isDebug }).catch(console.error) diff --git a/packages/mcp/src/mcp.js b/packages/mcp/src/mcp.js index 5b4603f..09373fb 100644 --- a/packages/mcp/src/mcp.js +++ b/packages/mcp/src/mcp.js @@ -4,6 +4,14 @@ import { z } from 'zod' import { searchDocs } from './searchDocs.js' import { version } from './version.js' +const LOG_FILE = path.join(process.cwd(), 'temba-mcp.log') + +function log(message) { + const timestamp = new Date().toISOString() + const entry = `[${timestamp}] ${message}\n` + fs.appendFileSync(LOG_FILE, entry) +} + let index = [] let lastFetched = 0 const CACHE_TTL = 3600000 // 1 hour in milliseconds @@ -30,7 +38,7 @@ async function ensureFreshIndex() { } } -export const startMcpServer = async () => { +export const startMcpServer = async ({ debug = false } = {}) => { const server = new McpServer({ name: 'temba-docs-mcp', version, @@ -45,6 +53,10 @@ export const startMcpServer = async () => { await ensureFreshIndex() const results = searchDocs(query, index).slice(0, 5) // Limit to top 5 results + if (debug) { + log(`Query: "${query}" | Results: ${results.length}`) + } + if (results.length === 0) { return { content: [{ type: 'text', text: 'No documentation found for your query.' }], From 7b68ada544713e69b237d15154d244b7f85d88b5 Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Fri, 5 Jun 2026 10:09:16 +0200 Subject: [PATCH 20/33] temba-mcp 0.3.0 --- package-lock.json | 2 +- packages/mcp/package.json | 2 +- packages/mcp/version.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9f395dd..4a6c4ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18804,7 +18804,7 @@ }, "packages/mcp": { "name": "temba-mcp", - "version": "0.2.0", + "version": "0.3.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "zod": "4.4.3" diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 406b930..463e51b 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "temba-mcp", - "version": "0.2.0", + "version": "0.3.0", "description": "MCP for Temba documentation", "author": "Bouwe (https://bouwe.io)", "scripts": { diff --git a/packages/mcp/version.js b/packages/mcp/version.js index f11a5bd..aa9835f 100644 --- a/packages/mcp/version.js +++ b/packages/mcp/version.js @@ -1 +1 @@ -export const version = '0.2.0' +export const version = '0.3.0' From e2a6ff5d2db4fe7031d4db856d62c6ab492fd707 Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Fri, 5 Jun 2026 10:17:44 +0200 Subject: [PATCH 21/33] fix: imports --- packages/mcp/src/mcp.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/mcp/src/mcp.js b/packages/mcp/src/mcp.js index 09373fb..a1c92b0 100644 --- a/packages/mcp/src/mcp.js +++ b/packages/mcp/src/mcp.js @@ -1,5 +1,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' +import fs from 'fs' +import path from 'path' import { z } from 'zod' import { searchDocs } from './searchDocs.js' import { version } from './version.js' From 6e4027b16a70507fcf24e544c51269930aba9912 Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Fri, 5 Jun 2026 10:18:16 +0200 Subject: [PATCH 22/33] temba-mcp 0.3.1 --- package-lock.json | 2 +- packages/mcp/package.json | 2 +- packages/mcp/version.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4a6c4ce..16d968b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18804,7 +18804,7 @@ }, "packages/mcp": { "name": "temba-mcp", - "version": "0.3.0", + "version": "0.3.1", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "zod": "4.4.3" diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 463e51b..e363c91 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "temba-mcp", - "version": "0.3.0", + "version": "0.3.1", "description": "MCP for Temba documentation", "author": "Bouwe (https://bouwe.io)", "scripts": { diff --git a/packages/mcp/version.js b/packages/mcp/version.js index aa9835f..9a71e44 100644 --- a/packages/mcp/version.js +++ b/packages/mcp/version.js @@ -1 +1 @@ -export const version = '0.3.0' +export const version = '0.3.1' From 86a0c761b90ae8f288fedf1054b5e90552f6d9f6 Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Fri, 5 Jun 2026 10:41:39 +0200 Subject: [PATCH 23/33] feat: add more debug logging --- packages/mcp/src/log.js | 11 +++++++++++ packages/mcp/src/mcp.js | 32 +++++++++++++++----------------- 2 files changed, 26 insertions(+), 17 deletions(-) create mode 100644 packages/mcp/src/log.js diff --git a/packages/mcp/src/log.js b/packages/mcp/src/log.js new file mode 100644 index 0000000..b353834 --- /dev/null +++ b/packages/mcp/src/log.js @@ -0,0 +1,11 @@ +const LOG_FILE = path.join(process.cwd(), 'temba-mcp.log') + +export const createLogger = (debug = false) => { + return (message) => { + if (debug) { + const timestamp = new Date().toISOString() + const entry = `[${timestamp}] ${message}\n` + fs.appendFileSync(LOG_FILE, entry) + } + } +} diff --git a/packages/mcp/src/mcp.js b/packages/mcp/src/mcp.js index a1c92b0..d74afcc 100644 --- a/packages/mcp/src/mcp.js +++ b/packages/mcp/src/mcp.js @@ -1,25 +1,16 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' -import fs from 'fs' -import path from 'path' import { z } from 'zod' +import { createLogger } from './log.js' import { searchDocs } from './searchDocs.js' import { version } from './version.js' -const LOG_FILE = path.join(process.cwd(), 'temba-mcp.log') - -function log(message) { - const timestamp = new Date().toISOString() - const entry = `[${timestamp}] ${message}\n` - fs.appendFileSync(LOG_FILE, entry) -} - let index = [] let lastFetched = 0 const CACHE_TTL = 3600000 // 1 hour in milliseconds const searchIndexUrl = 'https://docs.temba.io/search-index.json' -async function ensureFreshIndex() { +async function ensureFreshIndex(log) { if (Date.now() - lastFetched < CACHE_TTL && index.length > 0) return try { @@ -36,7 +27,7 @@ async function ensureFreshIndex() { index = await response.json() lastFetched = Date.now() } catch (e) { - console.error('Refresh failed, using stale index:', e) + log(`Refresh failed, using stale index: ${e.message}`) } } @@ -46,22 +37,29 @@ export const startMcpServer = async ({ debug = false } = {}) => { version, }) + const log = createLogger(debug) + // Register the tool server.tool( 'search_docs', 'Search the library documentation', { query: z.string() }, async ({ query }) => { - await ensureFreshIndex() + await ensureFreshIndex(log) + + log(`Current index size: ${index.length}`) + log(`First title: ${index[0]?.title}`) + const results = searchDocs(query, index).slice(0, 5) // Limit to top 5 results - if (debug) { - log(`Query: "${query}" | Results: ${results.length}`) - } + log(`Query: "${query}" | Results: ${results.length}`) + // Return a friendly message instead of an empty result to avoid LLM confusion. if (results.length === 0) { return { - content: [{ type: 'text', text: 'No documentation found for your query.' }], + content: [ + { type: 'text', text: `No Temba documentation found for your query "${query}".` }, + ], } } From c3d0530629402a9eee9b298fafd91bfaf17c3cb9 Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Fri, 5 Jun 2026 10:42:15 +0200 Subject: [PATCH 24/33] temba-mcp 0.3.2 --- package-lock.json | 2 +- packages/mcp/package.json | 2 +- packages/mcp/version.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16d968b..4065d77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18804,7 +18804,7 @@ }, "packages/mcp": { "name": "temba-mcp", - "version": "0.3.1", + "version": "0.3.2", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "zod": "4.4.3" diff --git a/packages/mcp/package.json b/packages/mcp/package.json index e363c91..17096b1 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "temba-mcp", - "version": "0.3.1", + "version": "0.3.2", "description": "MCP for Temba documentation", "author": "Bouwe (https://bouwe.io)", "scripts": { diff --git a/packages/mcp/version.js b/packages/mcp/version.js index 9a71e44..c2b325a 100644 --- a/packages/mcp/version.js +++ b/packages/mcp/version.js @@ -1 +1 @@ -export const version = '0.3.1' +export const version = '0.3.2' From 0ce18cbcb574eefef2be792382a846af0f3960bf Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Fri, 5 Jun 2026 11:23:46 +0200 Subject: [PATCH 25/33] feat: add more debug logging --- packages/mcp/src/log.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/mcp/src/log.js b/packages/mcp/src/log.js index b353834..5e50023 100644 --- a/packages/mcp/src/log.js +++ b/packages/mcp/src/log.js @@ -1,3 +1,6 @@ +import fs from 'fs' +import path from 'path' + const LOG_FILE = path.join(process.cwd(), 'temba-mcp.log') export const createLogger = (debug = false) => { From 7edda6d41d80cece3c3824430ea143271a9b7aff Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Fri, 5 Jun 2026 11:24:13 +0200 Subject: [PATCH 26/33] temba-mcp 0.3.3 --- package-lock.json | 2 +- packages/mcp/package.json | 2 +- packages/mcp/version.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4065d77..a6b231b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18804,7 +18804,7 @@ }, "packages/mcp": { "name": "temba-mcp", - "version": "0.3.2", + "version": "0.3.3", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "zod": "4.4.3" diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 17096b1..0b00e77 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "temba-mcp", - "version": "0.3.2", + "version": "0.3.3", "description": "MCP for Temba documentation", "author": "Bouwe (https://bouwe.io)", "scripts": { diff --git a/packages/mcp/version.js b/packages/mcp/version.js index c2b325a..5c54b77 100644 --- a/packages/mcp/version.js +++ b/packages/mcp/version.js @@ -1 +1 @@ -export const version = '0.3.2' +export const version = '0.3.3' From 4d5d28ee32bbc2385759424cdb18b339450dad39 Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Fri, 5 Jun 2026 11:27:12 +0200 Subject: [PATCH 27/33] fix: search_index URL --- packages/mcp/src/mcp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp/src/mcp.js b/packages/mcp/src/mcp.js index d74afcc..47f18ce 100644 --- a/packages/mcp/src/mcp.js +++ b/packages/mcp/src/mcp.js @@ -8,7 +8,7 @@ import { version } from './version.js' let index = [] let lastFetched = 0 const CACHE_TTL = 3600000 // 1 hour in milliseconds -const searchIndexUrl = 'https://docs.temba.io/search-index.json' +const searchIndexUrl = 'https://temba.bouwe.io/search_index.json' async function ensureFreshIndex(log) { if (Date.now() - lastFetched < CACHE_TTL && index.length > 0) return From b4254f9bd08821224e3e3b3f3edf67d54ffd8cbe Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Fri, 5 Jun 2026 11:27:34 +0200 Subject: [PATCH 28/33] temba-mcp 0.3.4 --- package-lock.json | 2 +- packages/mcp/package.json | 2 +- packages/mcp/version.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a6b231b..00ff461 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18804,7 +18804,7 @@ }, "packages/mcp": { "name": "temba-mcp", - "version": "0.3.3", + "version": "0.3.4", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "zod": "4.4.3" diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 0b00e77..50073f4 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "temba-mcp", - "version": "0.3.3", + "version": "0.3.4", "description": "MCP for Temba documentation", "author": "Bouwe (https://bouwe.io)", "scripts": { diff --git a/packages/mcp/version.js b/packages/mcp/version.js index 5c54b77..f44e6fc 100644 --- a/packages/mcp/version.js +++ b/packages/mcp/version.js @@ -1 +1 @@ -export const version = '0.3.3' +export const version = '0.3.4' From 1bd22e1b3e9a4cd16ff8c5c63c8037d0ddf87364 Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Fri, 5 Jun 2026 11:46:03 +0200 Subject: [PATCH 29/33] feat: improve debug logging --- packages/mcp/src/mcp.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/mcp/src/mcp.js b/packages/mcp/src/mcp.js index 47f18ce..8966de9 100644 --- a/packages/mcp/src/mcp.js +++ b/packages/mcp/src/mcp.js @@ -47,8 +47,17 @@ export const startMcpServer = async ({ debug = false } = {}) => { async ({ query }) => { await ensureFreshIndex(log) - log(`Current index size: ${index.length}`) - log(`First title: ${index[0]?.title}`) + if (index.length === 0) { + log('No documentation index available to search.') + return { + content: [ + { + type: 'text', + text: 'Documentation is currently unavailable. Please try again later.', + }, + ], + } + } const results = searchDocs(query, index).slice(0, 5) // Limit to top 5 results From 7e8a837ea43004172b24889f0e8036702e0368ff Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Fri, 5 Jun 2026 11:46:34 +0200 Subject: [PATCH 30/33] temba-mcp 0.3.5 --- package-lock.json | 2 +- packages/mcp/package.json | 2 +- packages/mcp/version.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 00ff461..625f6e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18804,7 +18804,7 @@ }, "packages/mcp": { "name": "temba-mcp", - "version": "0.3.4", + "version": "0.3.5", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "zod": "4.4.3" diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 50073f4..aae1d75 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "temba-mcp", - "version": "0.3.4", + "version": "0.3.5", "description": "MCP for Temba documentation", "author": "Bouwe (https://bouwe.io)", "scripts": { diff --git a/packages/mcp/version.js b/packages/mcp/version.js index f44e6fc..5633b0f 100644 --- a/packages/mcp/version.js +++ b/packages/mcp/version.js @@ -1 +1 @@ -export const version = '0.3.4' +export const version = '0.3.5' From 6f3ba937997cae899928df6a31ad67e166735b5e Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Fri, 5 Jun 2026 12:18:57 +0200 Subject: [PATCH 31/33] feat: make search tokenized and return ranked results --- packages/mcp/src/searchDocs.js | 69 +++++++++++++++--- packages/mcp/test/searchDocs.test.js | 100 ++++++++++++++++----------- 2 files changed, 121 insertions(+), 48 deletions(-) diff --git a/packages/mcp/src/searchDocs.js b/packages/mcp/src/searchDocs.js index 228d9bc..452d8a9 100644 --- a/packages/mcp/src/searchDocs.js +++ b/packages/mcp/src/searchDocs.js @@ -1,16 +1,67 @@ +const stopWords = new Set([ + 'a', + 'an', + 'the', + 'is', + 'i', + 'do', + 'how', + 'to', + 'and', + 'or', + 'of', + 'in', + 'on', + 'for', + 'with', + 'by', + 'as', + 'at', + 'from', + 'that', + 'this', + 'it', + 'are', + 'was', + 'were', + 'be', + 'been', +]) + export const searchDocs = (query, index) => { const lowerQuery = query?.toLowerCase().trim() + if (!lowerQuery) return [] + + const tokens = lowerQuery.split(/\s+/).filter((token) => !stopWords.has(token)) - if (!lowerQuery) { - return [] - } + // Early return if the user only searched for stop words + if (tokens.length === 0) return [] return ( - index?.filter( - (page) => - page.content.toLowerCase().includes(lowerQuery) || - page.title.toLowerCase().includes(lowerQuery) || - (page.keywords && page.keywords.some((k) => k.toLowerCase().includes(lowerQuery))), - ) || [] + index + .map((page) => { + const content = (page.content || '').toLowerCase() + const title = (page.title || '').toLowerCase() + const keywords = (page.keywords || []).map((k) => k.toLowerCase()) + + // Count how many tokens are found in this page + let score = 0 + tokens.forEach((token) => { + if ( + content.includes(token) || + title.includes(token) || + keywords.some((k) => k.includes(token)) + ) { + score += 1 + } + }) + + return { page, score } + }) + // Filter out pages that didn't match at least one token + .filter((item) => item.score > 0) + // Sort by most matches first + .sort((a, b) => b.score - a.score) + .map((item) => item.page) ) } diff --git a/packages/mcp/test/searchDocs.test.js b/packages/mcp/test/searchDocs.test.js index bdc6692..ced58e5 100644 --- a/packages/mcp/test/searchDocs.test.js +++ b/packages/mcp/test/searchDocs.test.js @@ -23,65 +23,87 @@ describe('searchDocs', () => { test('Returns no results for an empty query', () => { expect(searchDocs('', search_index)).toEqual([]) }) - test('Trims query whitespace', () => { - expect(searchDocs(' hello ', search_index)).toEqual([helloDocument]) - }) - test('Finds no results by title', async () => { - const result = searchDocs('goodbye', search_index) - expect(result).toEqual([]) - }) - test('Finds no results by content', async () => { - const result = searchDocs("Let's talk about saying goodbye to our planet.", search_index) - expect(result).toEqual([]) - }) - test('Finds no results by keyword', async () => { - const result = searchDocs('goodbye', search_index) - expect(result).toEqual([]) + test('Finds no results', () => { + expect(searchDocs('goodbye', search_index)).toEqual([]) }) }) describe('Finding 1 document', () => { - test('Finds 1 result by title', async () => { - const result = searchDocs('hello', search_index) - expect(result).toEqual([helloDocument]) + test('Finds by title', () => { + expect(searchDocs('hello', search_index)).toEqual([helloDocument]) }) - test('Finds 1 result by content', async () => { - const result = searchDocs("Let's talk about greeting our great planet.", search_index) - expect(result).toEqual([helloDocument]) + test('Finds by content (partial)', () => { + // Testing partial token match instead of full sentence + expect(searchDocs('greeting', search_index)).toEqual([helloDocument]) }) - test('Finds 1 result by keyword', async () => { - const result = searchDocs('earth', search_index) - expect(result).toEqual([helloDocument]) + test('Finds by keyword', () => { + expect(searchDocs('earth', search_index)).toEqual([helloDocument]) + }) + test('Finds by trimming query whitespace', () => { + expect(searchDocs(' hello ', search_index)).toEqual([helloDocument]) }) }) describe('Finding multiple documents', () => { - test('Finds 2 results by title', async () => { - const result = searchDocs('wOrld', search_index) - expect(result).toEqual([helloDocument, scotlandDocument]) + test('Finds 2 results by title token', () => { + // Using 'world' which appears in both titles + const result = searchDocs('world', search_index) + expect(result).toEqual(expect.arrayContaining([helloDocument, scotlandDocument])) }) - test('Finds 2 results by content', async () => { + test('Finds 2 results by content token', () => { const result = searchDocs('great', search_index) - expect(result).toEqual([helloDocument, scotlandDocument]) + expect(result).toEqual(expect.arrayContaining([helloDocument, scotlandDocument])) }) - test('Finds 2 results by keyword', async () => { + test('Finds 2 results by keyword', () => { const result = searchDocs('howdy', search_index) - expect(result).toEqual([helloDocument, scotlandDocument]) + expect(result).toEqual(expect.arrayContaining([helloDocument, scotlandDocument])) }) }) describe('Finding partial matches', () => { - test('Finds partial matches by title', async () => { - const result = searchDocs('greet', search_index) - expect(result).toEqual([helloDocument]) + test('Finds partial matches by title', () => { + expect(searchDocs('greet', search_index)).toEqual([helloDocument]) + }) + test('Finds partial matches by content', () => { + expect(searchDocs('mountain', search_index)).toEqual([scotlandDocument]) + }) + test('Finds partial matches by keyword', () => { + expect(searchDocs('bagpipe', search_index)).toEqual([scotlandDocument]) + }) + }) + + describe('Edge Cases and Ranking', () => { + test('Ranks documents with more matches higher', () => { + // hello matches 'world', 'great' (2 matches) + // scotland matches 'great' (1 match) + const result = searchDocs('world great', search_index) + expect(result[0].title).toBe('Hello World') }) - test('Finds partial matches by content', async () => { - const result = searchDocs('mountain', search_index) - expect(result).toEqual([scotlandDocument]) + + test('Handles pages with missing fields gracefully', () => { + const brokenDoc = { title: 'Broken' } // Missing content and keywords + const index = [brokenDoc] + expect(() => searchDocs('broken', index)).not.toThrow() + expect(searchDocs('broken', index)).toEqual([brokenDoc]) }) - test('Finds partial matches by keyword', async () => { - const result = searchDocs('bagpipe', search_index) - expect(result).toEqual([scotlandDocument]) + }) + + describe('Stop words filtering', () => { + test('Filters out stop words from the query', () => { + // "the" and "is" are stop words. "world" is the only active token. + // Both docs contain "world" in their title or content. + const result = searchDocs('the world is', search_index) + expect(result).toEqual(expect.arrayContaining([helloDocument, scotlandDocument])) + }) + + test('Returns no results if query only contains stop words', () => { + // Should return [] because no tokens remain after filtering + expect(searchDocs('the is a', search_index)).toEqual([]) + }) + + test('Still finds relevant docs when stop words are present', () => { + // "planet" is the keyword. "in the" is noise. + expect(searchDocs('planet in the', search_index)).toEqual([helloDocument]) }) }) }) From 341c7430b43d3256be65e3b5ba13080ad6977681 Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Fri, 5 Jun 2026 12:19:59 +0200 Subject: [PATCH 32/33] temba-mcp 0.4.0 --- package-lock.json | 2 +- packages/mcp/package.json | 2 +- packages/mcp/version.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 625f6e4..fde18b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18804,7 +18804,7 @@ }, "packages/mcp": { "name": "temba-mcp", - "version": "0.3.5", + "version": "0.4.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "zod": "4.4.3" diff --git a/packages/mcp/package.json b/packages/mcp/package.json index aae1d75..0194701 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "temba-mcp", - "version": "0.3.5", + "version": "0.4.0", "description": "MCP for Temba documentation", "author": "Bouwe (https://bouwe.io)", "scripts": { diff --git a/packages/mcp/version.js b/packages/mcp/version.js index 5633b0f..02968f1 100644 --- a/packages/mcp/version.js +++ b/packages/mcp/version.js @@ -1 +1 @@ -export const version = '0.3.5' +export const version = '0.4.0' From 67a390e96e5f78e02e9653dd10caf59d8233d3ac Mon Sep 17 00:00:00 2001 From: "Bouwe K. Westerdijk" Date: Fri, 5 Jun 2026 14:42:24 +0200 Subject: [PATCH 33/33] chore: backdoor querying --- packages/mcp/src/cli.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/mcp/src/cli.js b/packages/mcp/src/cli.js index 9936146..c0be97a 100755 --- a/packages/mcp/src/cli.js +++ b/packages/mcp/src/cli.js @@ -1,11 +1,35 @@ #!/usr/bin/env node import { startMcpServer } from './mcp.js' +import { searchDocs } from './searchDocs.js' const args = process.argv.slice(2) const isDebug = args.includes('--debug') +const queryIndex = args.findIndex((a) => a === '-q' || a === '--query') if (isDebug) { console.error('✨ Temba Docs MCP running in DEBUG mode') } +// Check for the testing flag +if (queryIndex !== -1 && args[queryIndex + 1]) { + const query = args[queryIndex + 1] + + try { + // 1. Fetch from the exact same remote location the MCP server uses + const response = await fetch('https://temba.bouwe.io/search_index.json') + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`) + const index = await response.json() + + // 2. Execute logic + const results = searchDocs(query, index) + + // 3. Output raw JSON (Agent-fidelity) + process.stdout.write(JSON.stringify(results, null, 2)) + process.exit(0) + } catch (err) { + console.error('❌ Failed to fetch remote index for test:', err.message) + process.exit(1) + } +} + startMcpServer({ debug: isDebug }).catch(console.error)