|
1 | | -import { join } from 'node:path'; |
2 | | -import { isNull } from '@aws-lambda-powertools/commons/typeutils'; |
3 | 1 | 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'; |
7 | 2 | import { logger } from '../../logger.ts'; |
8 | 3 | import { buildResponse } from '../shared/buildResponse.ts'; |
| 4 | +import { fetchWithCache } from '../shared/fetchWithCache.ts'; |
9 | 5 | 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'; |
13 | 7 |
|
14 | 8 | /** |
15 | 9 | * Fetch a documentation page from remote or local cache. |
16 | 10 | * |
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. |
28 | 12 | * |
29 | 13 | * @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 |
31 | 15 | */ |
32 | | -const tool = async (props: { |
33 | | - url: z.infer<typeof schema.url>; |
34 | | -}): Promise<CallToolResult> => { |
| 16 | +const tool = async (props: ToolProps): Promise<CallToolResult> => { |
35 | 17 | const { url } = props; |
36 | 18 | logger.appendKeys({ tool: toolName }); |
37 | 19 | logger.appendKeys({ url: url.toString() }); |
38 | 20 |
|
39 | | - const cachePath = join(CACHE_BASE_PATH, 'markdown-cache'); |
40 | | - const cacheKey = url.pathname; |
41 | | - logger.debug('Generated cache key', { cacheKey }); |
42 | | - |
43 | 21 | 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 | + }); |
77 | 25 | } 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 | + }); |
99 | 30 | /* v8 ignore start */ |
100 | 31 | } finally { |
101 | | - /* v8 ignore end */ |
102 | | - logger.removeKeys(['url', 'tool']); |
| 32 | + /* v8 ignore stop */ |
| 33 | + logger.removeKeys(['tool', 'url']); |
103 | 34 | } |
104 | 35 | }; |
105 | 36 |
|
|
0 commit comments