diff --git a/biome.json b/biome.json index 9279d59..397f323 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json", + "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", "assist": { "actions": { "source": { "organizeImports": "on" } } }, "linter": { "enabled": true, diff --git a/package-lock.json b/package-lock.json index 213b360..3cddb4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2462,6 +2462,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.10.0" } @@ -3750,6 +3751,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -4384,18 +4386,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -4952,6 +4942,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@bundled-es-modules/cookie": "^2.0.1", "@bundled-es-modules/statuses": "^1.0.1", @@ -5358,6 +5349,7 @@ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -5371,6 +5363,7 @@ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -6321,6 +6314,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6507,6 +6501,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6630,6 +6625,7 @@ "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -6746,6 +6742,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6759,6 +6756,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -7146,6 +7144,7 @@ "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index fee299a..ee2ef6a 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "test:unit": "vitest --run tests/unit", "test:e2e": "vitest --run tests/e2e", "test:unit:coverage": "vitest --run tests/unit --coverage.enabled --coverage.thresholds.100 --coverage.include='src/**'", - "prebuild": "rimraf dist/*", + "prebuild": "rimraf dist/* tsconfig.tsbuildinfo", "build": "tsc --build", "lint": "biome lint .", "lint:fix": "biome check --write .", diff --git a/src/constants.ts b/src/constants.ts index e37d996..1d47ab8 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -8,9 +8,9 @@ import { const MCP_SERVER_NAME = 'powertools-for-aws-mcp' as const; // Allowed domain for security -const ALLOWED_DOMAIN = 'docs.powertools.aws.dev'; +const ALLOWED_DOMAIN = 'docs.aws.amazon.com'; // Base URL for Powertools documentation -const POWERTOOLS_BASE_URL = 'https://docs.powertools.aws.dev/lambda'; +const POWERTOOLS_BASE_URL = 'https://docs.aws.amazon.com/powertools'; const FETCH_TIMEOUT_MS = 15000; // 15 seconds timeout for fetch operations diff --git a/src/tools/searchDocs/tool.ts b/src/tools/searchDocs/tool.ts index 7056047..5568c9a 100644 --- a/src/tools/searchDocs/tool.ts +++ b/src/tools/searchDocs/tool.ts @@ -3,12 +3,11 @@ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import lunr from 'lunr'; import { POWERTOOLS_BASE_URL, - SEARCH_CONFIDENCE_THRESHOLD, } from '../../constants.ts'; import { logger } from '../../logger.ts'; import { buildResponse } from '../shared/buildResponse.ts'; import { fetchWithCache } from '../shared/fetchWithCache.ts'; -import type { ToolProps } from './types.ts'; +import type { ToolProps, MkDocsSearchIndex } from './types.ts'; /** * Search for documentation based on the provided parameters. @@ -16,9 +15,9 @@ import type { ToolProps } from './types.ts'; * This tool fetches a search index from the Powertools for AWS documentation, * hydrates it into a Lunr index, and performs a search based on the provided query. * - * The search index is expected to be in a specific format, and the results - * are filtered based on a confidence threshold to ensure relevance. This threshold - * can be configured via the `SEARCH_CONFIDENCE_THRESHOLD` environment variable. + * The search index is expected to be in the full MkDocs Material format with proper + * field boosting (1000x for titles, 1M for tags) to match the online search experience. + * Results are filtered based on a confidence threshold to ensure relevance. * * This tool is designed to work with the Powertools for AWS documentation * for various runtimes, including Python and TypeScript, and supports versioning. @@ -45,19 +44,36 @@ const tool = async (props: ToolProps): Promise => { const urlSuffix = '/search/search_index.json'; url.pathname = `${url.pathname}${urlSuffix}`; - let searchIndexContent: { - docs: { location: string; title: string; text: string }[]; - }; + let searchIndex: MkDocsSearchIndex; try { const content = await fetchWithCache({ url, contentType: 'application/json', }); - searchIndexContent = JSON.parse(content); - if ( - isNullOrUndefined(searchIndexContent.docs) || - !Array.isArray(searchIndexContent.docs) - ) { + const rawIndex = JSON.parse(content); + + // Handle both full MkDocs index and simplified format for backward compatibility + if (rawIndex.docs && Array.isArray(rawIndex.docs)) { + if (rawIndex.config?.lang) { + // Full MkDocs search index format + searchIndex = rawIndex as MkDocsSearchIndex; + } else { + // Simplified format - convert to full structure + searchIndex = { + config: { + lang: ['en'], + separator: '[\\s\\-]+', + pipeline: ['stopWordFilter', 'stemmer'] + }, + docs: rawIndex.docs, + options: { suggest: false } + }; + } + } else { + throw new Error('Invalid search index format: missing docs array'); + } + + if (isNullOrUndefined(searchIndex.docs) || !Array.isArray(searchIndex.docs)) { throw new Error( `Invalid search index format for ${runtime} ${version}: missing 'docs' property` ); @@ -72,26 +88,42 @@ const tool = async (props: ToolProps): Promise => { }); } - // TODO: consume built/exported search index - #79 + // Build Lunr index with proper MkDocs Material configuration const index = lunr(function () { + // Apply language configuration if not English + if (searchIndex.config.lang.length === 1 && searchIndex.config.lang[0] !== "en") { + // Note: This would require language-specific Lunr plugins + logger.debug(`Language configuration detected: ${searchIndex.config.lang[0]}`); + } else if (searchIndex.config.lang.length > 1) { + logger.debug(`Multi-language configuration detected: ${searchIndex.config.lang.join(', ')}`); + } + this.ref('location'); - this.field('title', { boost: 10 }); - this.field('text'); + // Use proper MkDocs Material field boosting + this.field('title', { boost: 1000 }); // 1000x boost for titles + this.field('text', { boost: 1 }); // 1x boost for text + this.field('tags', { boost: 1000000 }); // 1M boost for tags - for (const doc of searchIndexContent.docs) { + for (const doc of searchIndex.docs) { if (!doc.location || !doc.title || !doc.text) continue; - this.add({ + const indexDoc: Record = { location: doc.location, title: doc.title, text: doc.text, - }); + }; + + // Add tags if present + if (doc.tags && doc.tags.length > 0) { + indexDoc.tags = doc.tags.join(' '); + } + + this.add(indexDoc, { boost: doc.boost || 1 }); } }); const results = []; for (const result of index.search(search)) { - if (result.score < SEARCH_CONFIDENCE_THRESHOLD) break; // Results are sorted by score, so we can stop early results.push({ title: result.ref, url: `${baseUrl}/${result.ref}`, @@ -99,9 +131,7 @@ const tool = async (props: ToolProps): Promise => { }); } - logger.debug( - `Search results with confidence >= ${SEARCH_CONFIDENCE_THRESHOLD} found: ${results.length}` - ); + logger.debug(`Search results found: ${results.length}`); return buildResponse({ content: results, diff --git a/src/tools/searchDocs/types.ts b/src/tools/searchDocs/types.ts index 52aee31..7ba8b47 100644 --- a/src/tools/searchDocs/types.ts +++ b/src/tools/searchDocs/types.ts @@ -8,18 +8,29 @@ type ToolProps = { }; // Define the structure of MkDocs search index +interface SearchConfig { + lang: string[]; + separator: string; + pipeline: string[]; +} + +interface SearchDocument { + location: string; + title: string; + text: string; + tags?: string[]; + boost?: number; + parent?: SearchDocument; +} + +interface SearchOptions { + suggest: boolean; +} + interface MkDocsSearchIndex { - config: { - lang: string[]; - separator: string; - pipeline: string[]; - }; - docs: Array<{ - location: string; - title: string; - text: string; - tags?: string[]; - }>; + config: SearchConfig; + docs: SearchDocument[]; + options?: SearchOptions; } -export type { ToolProps, MkDocsSearchIndex }; +export type { ToolProps, MkDocsSearchIndex, SearchConfig, SearchDocument, SearchOptions }; diff --git a/tests/e2e/stdio.test.ts b/tests/e2e/stdio.test.ts index 745bd45..bb9986a 100644 --- a/tests/e2e/stdio.test.ts +++ b/tests/e2e/stdio.test.ts @@ -47,6 +47,8 @@ describe('MCP Server e2e (child process)', () => { }, }, }); + console.error('hello') + console.log(response.content[0].text); // Assess expect(response.content[0].type).toBe('text'); diff --git a/tests/unit/searchDocs.test.ts b/tests/unit/searchDocs.test.ts index 01e2e21..4b317fd 100644 --- a/tests/unit/searchDocs.test.ts +++ b/tests/unit/searchDocs.test.ts @@ -34,11 +34,17 @@ describe('tool', () => { () => HttpResponse.text( JSON.stringify({ + config: { + lang: ['en'], + separator: '[\\s\\-]+', + pipeline: ['stopWordFilter', 'stemmer'] + }, docs: [ { location: 'features/logger/#buffering-logs', title: 'Buffering logs', text: "

Log buffering enables you to buffer logs for a specific request or invocation. Enable log buffering by passing logBufferOptions when initializing a Logger instance. You can buffer logs at the WARNING, INFO, DEBUG, or TRACE level, and flush them automatically on error or manually as needed.

This is useful when you want to reduce the number of log messages emitted while still having detailed logs when needed, such as when troubleshooting issues.

logBufferingGettingStarted.ts
import { Logger } from '@aws-lambda-powertools/logger';\n\nconst logger = new Logger({\n  logBufferOptions: {\n    maxBytes: 20480,\n    flushOnErrorLog: true,\n  },\n});\n\nlogger.debug('This is a debug message'); // This is NOT buffered\n\nexport const handler = async () => {\n  logger.debug('This is a debug message'); // This is buffered\n  logger.info('This is an info message');\n\n  // your business logic here\n\n  logger.error('This is an error message'); // This also flushes the buffer\n  // or logger.flushBuffer(); // to flush the buffer manually\n};\n
", + tags: ['logger', 'buffering', 'debug'] }, { location: 'features/logger/#configuring-the-buffer', @@ -175,7 +181,7 @@ describe('tool', () => { // Assess expect(result.content).toBeResponseWithText( - `Failed to fetch search index for java latest: Invalid search index format for java latest: missing 'docs' property` + 'Failed to fetch search index for java latest: Invalid search index format: missing docs array' ); expect(result.isError).toBe(true); });