Skip to content

Commit 29cf568

Browse files
committed
feat: refactor to trade one look up module tool for the 50+ dynamically registered tools
Signed-off-by: Liran Tal <liran.tal@gmail.com>
1 parent b4d545d commit 29cf568

File tree

8 files changed

+101
-31
lines changed

8 files changed

+101
-31
lines changed

src/bin/cli.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
#!/usr/bin/env node
21
import { debuglog } from 'node:util'
32
import { startServer } from 'src/main.ts'
43
const debug = debuglog('mcp-server-nodejs-api-docs')
54

65
debug('Starting Server...')
7-
startServer()
6+
startServer()

src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ export async function startServer (): Promise<void> {
2727
console.error('Fatal error during server transport init.')
2828
process.exit(1)
2929
}
30-
}
30+
}

src/resources/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { ReadResourceRequest } from '@modelcontextprotocol/sdk/types.js'
1010
const logger: Logger = initLogger()
1111
const cacheService = new CacheService()
1212

13-
export async function initializeResources(server: Server): Promise<void> {
13+
export async function initializeResources (server: Server): Promise<void> {
1414
logger.info({ msg: 'Initializing resources...' })
1515

1616
const resources = [

src/services/api-docs-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export class ApiDocsService {
7878
// Remove entries without Class or Method
7979
const originalCount = apiDocs.modules?.length
8080
apiDocs.modules = apiDocs.modules.filter(module =>
81-
module?.classes?.length && module.classes.length > 0 ||
81+
module?.classes?.length && module.classes.length > 0 ||
8282
module?.methods?.length && module.methods.length > 0
8383
)
8484
this.logger.info({ msg: `Modules count: ${originalCount}` })

src/services/cache-service.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export class CacheService {
1313
private logger: Logger
1414
private cache: Map<string, CacheEntry<any>>
1515

16-
constructor() {
16+
constructor () {
1717
this.logger = initLogger()
1818
this.cache = new Map()
1919
}
@@ -29,7 +29,7 @@ export class CacheService {
2929
): Promise<T> {
3030
const { ttlDays = 7 } = options
3131
const now = Date.now()
32-
32+
3333
// Check if we have valid cached data
3434
const cached = this.cache.get(key)
3535
if (cached && cached.expiresAt > now) {
@@ -41,19 +41,19 @@ export class CacheService {
4141
this.logger.info({ msg: `Cache miss for key: ${key}, fetching fresh data...` })
4242
try {
4343
const data = await fetcher()
44-
44+
4545
// Calculate expiration time
4646
const expiresAt = now + (ttlDays * 24 * 60 * 60 * 1000)
47-
47+
4848
// Store in cache
4949
this.cache.set(key, { data, expiresAt })
50-
51-
this.logger.info({
52-
msg: `Cached data for key: ${key}`,
50+
51+
this.logger.info({
52+
msg: `Cached data for key: ${key}`,
5353
expiresAt: new Date(expiresAt).toISOString(),
54-
ttlDays
54+
ttlDays
5555
})
56-
56+
5757
return data
5858
} catch (error) {
5959
this.logger.error({ err: error, msg: `Failed to fetch data for key: ${key}` })
@@ -64,22 +64,22 @@ export class CacheService {
6464
/**
6565
* Convenience method for fetching HTTP resources with caching
6666
*/
67-
async fetchHttpWithCache(
67+
async fetchHttpWithCache (
6868
url: string,
6969
options: CacheOptions & { responseType?: 'json' | 'text' } = {}
7070
): Promise<any> {
7171
const { responseType = 'json', ...cacheOptions } = options
72-
72+
7373
return this.fetchWithCache(
7474
url,
7575
async () => {
7676
this.logger.info({ msg: `Fetching HTTP resource: ${url}` })
7777
const response = await fetch(url)
78-
78+
7979
if (!response.ok) {
8080
throw new Error(`HTTP error! status: ${response.status} ${response.statusText}`)
8181
}
82-
82+
8383
const data = responseType === 'json' ? await response.json() : await response.text()
8484
this.logger.info({ msg: `Successfully fetched HTTP resource: ${url}` })
8585
return data
@@ -91,25 +91,25 @@ export class CacheService {
9191
/**
9292
* Clear a specific cache entry
9393
*/
94-
clearCache(key: string): void {
94+
clearCache (key: string): void {
9595
this.cache.delete(key)
9696
this.logger.info({ msg: `Cleared cache for key: ${key}` })
9797
}
9898

9999
/**
100100
* Clear all expired cache entries
101101
*/
102-
clearExpiredCache(): void {
102+
clearExpiredCache (): void {
103103
const now = Date.now()
104104
let clearedCount = 0
105-
105+
106106
for (const [key, entry] of this.cache.entries()) {
107107
if (entry.expiresAt <= now) {
108108
this.cache.delete(key)
109109
clearedCount++
110110
}
111111
}
112-
112+
113113
if (clearedCount > 0) {
114114
this.logger.info({ msg: `Cleared ${clearedCount} expired cache entries` })
115115
}
@@ -118,10 +118,10 @@ export class CacheService {
118118
/**
119119
* Get cache statistics
120120
*/
121-
getCacheStats(): { size: number; keys: string[] } {
121+
getCacheStats (): { size: number; keys: string[] } {
122122
return {
123123
size: this.cache.size,
124124
keys: Array.from(this.cache.keys())
125125
}
126126
}
127-
}
127+
}

src/services/docs-formatter-service.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ interface FormattingOptions {
3333
* Service responsible for formatting Node.js API documentation into readable markdown content
3434
*/
3535
export class DocsFormatter {
36-
// eslint-disable-next-line no-useless-constructor
3736
constructor () {
3837
// Initialize any needed properties here
3938
}
@@ -56,6 +55,7 @@ export class DocsFormatter {
5655

5756
formatModuleSummary (module: ApiModule): string {
5857
let content = `## ${module.displayName || module.textRaw} (${module.name})\n`
58+
content += `Module name or Class name: \`${module.name}\`\n\n`
5959

6060
if (
6161
(module.methods && module.methods.length > 0) ||
@@ -64,12 +64,12 @@ export class DocsFormatter {
6464
content += '### Methods\n'
6565

6666
module?.methods?.forEach((method) => {
67-
content += `#### ${method.textRaw}\n`
67+
content += `#### ${method.textRaw}\n`
6868
})
6969

7070
module?.modules?.forEach((submodules) => {
7171
submodules?.methods?.forEach((method) => {
72-
content += `#### ${method.textRaw}\n`
72+
content += `#### ${method.textRaw}\n`
7373
})
7474
})
7575
}

src/tools/index.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,23 @@ import {
44
CallToolRequestSchema,
55
} from '@modelcontextprotocol/sdk/types.js'
66

7-
import { createSearchTool, createModuleTools } from './tools-factory.ts'
7+
import { createSearchTool, createModulesTool } from './tools-factory.ts'
88

99
export async function initializeTools (server: Server): Promise<void> {
1010
// Create the search tool
1111
const searchTool = await createSearchTool()
1212

13-
// Create individual module tools
14-
const moduleTools = await createModuleTools()
13+
// Refactor to avoid the `createModuleTools` that created a single tool for each
14+
// module which resulted in potentially too many tools being registered (about 50)
15+
// and instead create a single tool that can handle all modules by using a sort of
16+
// look up method
17+
// const moduleTools = await createModuleTools()
18+
const modulesTool = await createModulesTool()
1519

1620
// Combine all tools
1721
const tools = {
1822
[searchTool.name]: searchTool,
19-
...moduleTools
23+
...modulesTool
2024
}
2125

2226
// Create tool list for MCP

src/tools/tools-factory.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ interface ToolDefinition {
1010
inputSchema: {
1111
type: string
1212
properties: Record<string, any>
13+
required?: string[]
1314
}
1415
handler: (params: Record<string, any>) => Promise<{ content: { type: string; text: string }[] }>
1516
}
@@ -44,6 +45,72 @@ export async function createSearchTool (): Promise<ToolDefinition> {
4445
}
4546
}
4647

48+
/**
49+
* One tool that serves documentation for all modules
50+
*
51+
* Instead of overwhelming the LLM with as many tools as there are core modules
52+
* in Node.js, this creates a single tool that accepts modules information
53+
* and returns their data as a response.
54+
*/
55+
export async function createModulesTool () {
56+
const toolName = 'api-docs-module-description'
57+
const toolDescription = 'Use this tool to retrieve Node.js API documentation for a specific module or class, including its methods and descriptions.'
58+
59+
const tools: ToolsDictionary = {}
60+
61+
const tool: ToolDefinition = {
62+
name: toolName,
63+
description: toolDescription,
64+
inputSchema: {
65+
type: 'object',
66+
properties: {
67+
module: {
68+
type: 'string',
69+
description: 'The module or class name to retrieve Node.js API documentation for',
70+
},
71+
method: {
72+
type: 'string',
73+
description: 'The method name to retrieve Node.js API documentation for',
74+
},
75+
},
76+
required: ['module'],
77+
},
78+
async handler (params) {
79+
logger.info({ msg: `Tool execution started: ${toolName}`, params })
80+
try {
81+
// TBD extract the module or class name out of all modules documentation
82+
const { modules } = await apiDocsService.getApiDocsModules()
83+
84+
// @TODO the tool only uses the module name to find a matching module
85+
// and doesn't match against the method name or the class name
86+
// probably good enough for results but open to improvements, especially
87+
// if there's more data points around methods and classes that should
88+
// be returned
89+
const module = modules.find((mod) => apiDocsService.normalizeModuleName(mod.name) === params.module)
90+
if (!module) {
91+
return {
92+
content: [{ type: 'text', text: `Module not found: ${params.module}\n\nMaybe you spelled the module name wrong?` }],
93+
}
94+
}
95+
96+
const content = await apiDocsService.getFormattedModuleDoc(module, { class: params.module, method: params.method })
97+
logger.info({ msg: `Tool execution successful: ${toolName}` })
98+
return { content: [{ type: 'text', text: content }] }
99+
} catch (error) {
100+
logger.error({
101+
err: error,
102+
params,
103+
msg: `Tool execution failed: ${toolName}`,
104+
})
105+
throw error
106+
}
107+
},
108+
}
109+
110+
tools[toolName] = tool
111+
return tools
112+
}
113+
47114
export async function createModuleTools () {
48115
const tools: ToolsDictionary = {}
49116
const { modules } = await apiDocsService.getApiDocsModules()

0 commit comments

Comments
 (0)