Skip to content

Commit d50a2f6

Browse files
dreamorosisvozza
andauthored
feat: refactor search docs tool (#83)
Co-authored-by: Stefano Vozza <[email protected]>
1 parent 79bac51 commit d50a2f6

File tree

19 files changed

+889
-1084
lines changed

19 files changed

+889
-1084
lines changed

.github/workflows/make-release.yml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,16 @@ concurrency:
2323
group: on-release-publish
2424

2525
jobs:
26-
# temporarily disabled until we address #48
27-
# run-unit-tests:
28-
# uses: ./.github/workflows/reusable-code-quality.yml
26+
run-unit-tests:
27+
uses: ./.github/workflows/reusable-code-quality.yml
2928
run-e2e-tests:
30-
# needs: run-unit-tests # uncomment when we re-enable the unit tests #48
3129
uses: ./.github/workflows/reusable-e2e.yml
3230
# This job publishes the packages to npm.
3331
# It uses the latest git commit sha as the version and ensures provenance with NPM_CONFIG_PROVENANCE flag.
3432
# We don't bump the version because we do that in the `make-version` workflow.
3533
# It also sets the RELEASE_VERSION output to be used by the next job to create a git tag.
3634
publish-npm:
37-
needs: run-e2e-tests
35+
needs: [run-e2e-tests, run-unit-tests]
3836
# Needed as recommended by npm docs on publishing with provenance https://docs.npmjs.com/generating-provenance-statements
3937
permissions:
4038
id-token: write

biome.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
"lib",
3232
"cdk.out",
3333
"site",
34-
".aws-sam"
34+
".aws-sam",
35+
"dist"
3536
]
3637
}
3738
}

package-lock.json

Lines changed: 0 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@
5757
"@types/node": "^24.0.0",
5858
"cacache": "^19.0.1",
5959
"lunr": "^2.3.9",
60-
"lunr-languages": "^1.14.0",
6160
"zod": "^3.25.57"
6261
},
6362
"devDependencies": {

src/constants.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { tmpdir } from 'node:os';
22
import { join } from 'node:path';
3-
import { getStringFromEnv } from '@aws-lambda-powertools/commons/utils/env';
3+
import {
4+
getNumberFromEnv,
5+
getStringFromEnv,
6+
} from '@aws-lambda-powertools/commons/utils/env';
47

58
const MCP_SERVER_NAME = 'powertools-for-aws-mcp' as const;
69

@@ -18,11 +21,20 @@ const CACHE_BASE_PATH = getStringFromEnv({
1821
defaultValue: join(tmpdir(), 'powertools-mcp'),
1922
});
2023

24+
/**
25+
* Threshold for search confidence out of 100.
26+
*/
27+
const SEARCH_CONFIDENCE_THRESHOLD = getNumberFromEnv({
28+
key: 'SEARCH_CONFIDENCE_THRESHOLD',
29+
defaultValue: 30,
30+
});
31+
2132
export {
2233
MCP_SERVER_NAME,
2334
ALLOWED_DOMAIN,
2435
FETCH_TIMEOUT_MS,
2536
runtimes,
2637
POWERTOOLS_BASE_URL,
2738
CACHE_BASE_PATH,
39+
SEARCH_CONFIDENCE_THRESHOLD,
2840
};

src/tools/fetchDocPage/errors.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/tools/fetchDocPage/tool.ts

Lines changed: 14 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,36 @@
1-
import { join } from 'node:path';
2-
import { isNull } from '@aws-lambda-powertools/commons/typeutils';
31
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
4-
import { get as getFromCache, put as writeToCache } from 'cacache';
5-
import type { z } from 'zod';
6-
import { CACHE_BASE_PATH } from '../../constants.ts';
72
import { logger } from '../../logger.ts';
83
import { buildResponse } from '../shared/buildResponse.ts';
4+
import { fetchWithCache } from '../shared/fetchWithCache.ts';
95
import { name as toolName } from './constants.ts';
10-
import { CacheError } from './errors.ts';
11-
import type { schema } from './schemas.ts';
12-
import { getRemotePage, getRemotePageETag } from './utils.ts';
6+
import type { ToolProps } from './types.ts';
137

148
/**
159
* Fetch a documentation page from remote or local cache.
1610
*
17-
* When using this function, we first check if the page is available in the local cache
18-
* by using the page url as the cache key prefix.
19-
*
20-
* If none is found, we fetch the page from the remote server and cache it locally,
21-
* then return its markdown content.
22-
*
23-
* If the local cache includes a potential match, we make a `HEAD` request to the remote server
24-
* and compare the ETag with the one stored in the local cache.
25-
*
26-
* If the ETag matches, we return the cached markdown content. If it doesn't match,
27-
* we fetch the entire page using a `GET` request and update the local cache with the new content.
11+
* @see - {@link fetchWithCache | `fetchWithCache`} for the implementation details on caching and fetching.
2812
*
2913
* @param props - options for fetching a documentation page
30-
* @param props.pageUrl - the URL of the documentation page to fetch
14+
* @param props.url - the URL of the documentation page to fetch
3115
*/
32-
const tool = async (props: {
33-
url: z.infer<typeof schema.url>;
34-
}): Promise<CallToolResult> => {
16+
const tool = async (props: ToolProps): Promise<CallToolResult> => {
3517
const { url } = props;
3618
logger.appendKeys({ tool: toolName });
3719
logger.appendKeys({ url: url.toString() });
3820

39-
const cachePath = join(CACHE_BASE_PATH, 'markdown-cache');
40-
const cacheKey = url.pathname;
41-
logger.debug('Generated cache key', { cacheKey });
42-
4321
try {
44-
const [cachedETagPromise, remoteETagPromise] = await Promise.allSettled([
45-
getFromCache(cachePath, `${cacheKey}-etag`),
46-
getRemotePageETag(url),
47-
]);
48-
const cachedETag =
49-
cachedETagPromise.status === 'fulfilled'
50-
? cachedETagPromise.value.data.toString()
51-
: null;
52-
const remoteETag =
53-
remoteETagPromise.status === 'fulfilled' ? remoteETagPromise.value : null;
54-
55-
if (isNull(cachedETag) && isNull(remoteETag)) {
56-
throw new CacheError(
57-
'No cached ETag and remote ETag found, fetching remote page'
58-
);
59-
}
60-
61-
if (cachedETag === remoteETag) {
62-
logger.debug('cached eTag matches, returning cached markdown');
63-
try {
64-
const cachedMarkdown = await getFromCache(cachePath, cacheKey);
65-
return buildResponse({
66-
content: cachedMarkdown.data.toString(),
67-
});
68-
} catch (error) {
69-
throw new CacheError(
70-
'Cached markdown not found even though ETag matches; cache may be corrupted'
71-
);
72-
}
73-
}
74-
throw new CacheError(
75-
`ETag mismatch: local ${cachedETag} vs remote ${remoteETag}; fetching remote page`
76-
);
22+
return buildResponse({
23+
content: await fetchWithCache({ url, contentType: 'text/markdown' }),
24+
});
7725
} catch (error) {
78-
if (error instanceof CacheError) {
79-
logger.debug(error.message, { cacheKey });
80-
}
81-
try {
82-
const { markdown, eTag: newEtag } = await getRemotePage(url);
83-
84-
await writeToCache(cachePath, `${cacheKey}-etag`, newEtag);
85-
await writeToCache(cachePath, cacheKey, markdown);
86-
87-
return buildResponse({
88-
content: markdown,
89-
});
90-
} catch (fetchError) {
91-
logger.error('Failed to fetch remote page', {
92-
error: fetchError,
93-
});
94-
return buildResponse({
95-
content: `${(fetchError as Error).message}`,
96-
isError: true,
97-
});
98-
}
26+
return buildResponse({
27+
content: `${(error as Error).message}`,
28+
isError: true,
29+
});
9930
/* v8 ignore start */
10031
} finally {
101-
/* v8 ignore end */
102-
logger.removeKeys(['url', 'tool']);
32+
/* v8 ignore stop */
33+
logger.removeKeys(['tool', 'url']);
10334
}
10435
};
10536

src/tools/fetchDocPage/utils.ts

Lines changed: 0 additions & 79 deletions
This file was deleted.

0 commit comments

Comments
 (0)