diff --git a/src/__tests__/__snapshots__/options.defaults.test.ts.snap b/src/__tests__/__snapshots__/options.defaults.test.ts.snap index 24cb856b..9ad46c84 100644 --- a/src/__tests__/__snapshots__/options.defaults.test.ts.snap +++ b/src/__tests__/__snapshots__/options.defaults.test.ts.snap @@ -2,6 +2,7 @@ exports[`options defaults should return specific properties: defaults 1`] = ` { + "contextManagement": false, "contextPath": "/", "contextUrl": "file:///", "docsPathSlug": "documentation:", @@ -30,6 +31,10 @@ exports[`options defaults should return specific properties: defaults 1`] = ` "max": 256, "min": 1, }, + "resourceSearches": { + "max": 15, + "min": 0, + }, "toolSearches": { "max": 10, "min": 0, diff --git a/src/__tests__/server.test.ts b/src/__tests__/server.test.ts index 554f5321..0731dbdb 100644 --- a/src/__tests__/server.test.ts +++ b/src/__tests__/server.test.ts @@ -82,13 +82,13 @@ describe('runServer', () => { it.each([ { description: 'use default tools, stdio', - options: { name: 'test-server-1', version: '1.0.0' }, + options: { name: 'test-server-1', version: '1.0.0', contextManagement: undefined }, tools: undefined, transportMethod: MockStdioServerTransport }, { description: 'use default tools, http', - options: { name: 'test-server-2', version: '1.0.0', isHttp: true }, + options: { name: 'test-server-2', version: '1.0.0', isHttp: true, contextManagement: false }, tools: undefined, transportMethod: MockStartHttpTransport }, diff --git a/src/mcpSdk.ts b/src/mcpSdk.ts index 7961aa5e..5e136d0a 100644 --- a/src/mcpSdk.ts +++ b/src/mcpSdk.ts @@ -4,14 +4,16 @@ import { type ResourceMetadata, type CompleteResourceTemplateCallback } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { type Tool } from '@modelcontextprotocol/sdk/types.js'; import { type GlobalOptions } from './options'; import { listAllCombinations, listIncrementalCombinations, splitUri } from './server.helpers'; /** * A tool registered with the MCP server. * - * @note Use of `any` here is intentional as part of a pass-through policy around - * `inputSchema`. Input schemas are actually reconstructed as part of the + * @note Use of `any` here is intentional as part of this typing. This is part of a general + * pass-through policy around our SDK types. + * - `inputSchema`: Input schemas are actually reconstructed as part of the * tools-as-plugins architecture to help guarantee that a minimal tool schema is * always available and minimally valid. * @@ -20,15 +22,22 @@ import { listAllCombinations, listIncrementalCombinations, splitUri } from './se * - `schema.description` `{string}`: Concise description of functionality for the tool. * - `schema.inputSchema` `{*}`: Internally, a raw Zod schema. Externally, a JSON or raw Zod schema. External tools are * converted to Zod for user convenience. - * 2. `handler` `{Function}`: Tool handler function for returning content. + * - `schema.annotations` `{Object}`: Optional annotations for the tool. + * 2. `handler` `{Function}`: Resource handler function for returning content. + * 3. `_config` `{Object}`: Internal Tool configuration. + * - `config.shouldRegister`: Optional callback to determine if the tool should be registered. */ type McpTool = [ name: string, schema: { description: string; inputSchema: any; + annotations?: Tool['annotations'] | any; }, - handler: (arg?: unknown) => any | Promise + handler: (arg?: unknown) => any | Promise, + _config?: { + shouldRegister?: (options: GlobalOptions) => boolean | Promise; + } ]; /** @@ -85,19 +94,35 @@ interface McpResourceMetadata { /** * A resource registered with the MCP server. * - * 0. `name`: Registered name of the resource. - * 1. `uriOrTemplate`: URI string or template. {@link ResourceTemplate} - * 2. `config`: Resource configuration metadata. {@link ResourceMetadata} - * 3. `handler`: Resource handler function. - * 4. `metadata`: Optional **internal metadata** object, not used by the standard MCP SDK + * 0. `name` `{string}`: Registered name of the resource. + * 1. `uriOrTemplate` `{string}`: URI string or template. {@link ResourceTemplate} + * 2. `config` `{Object}`: Resource configuration metadata. {@link ResourceMetadata} + * 3. `handler` `{Function}`: Resource handler function. + * 4. `metadata` `{Object}`: Optional **internal metadata** object, not used by the standard MCP SDK * resource registry. {@link McpResourceMetadata} + * 5. `_config` `{Object}`: Internal Resource configuration. + * - `_config.shouldRegister` `{Function|Promise}`: Optional callback to determine if the resource should be registered. + * + * @note Annotations help with prioritizing resources and help manage context. They contain 3 primary properties: + * - `priority`: A ranking from `0.0` to `1.0`. `1.0` being the highest priority, and `0.0` being the lowest. + * - `audience`: This can be `user` or `assistant`, possibly both. + * - `lastModified': an ISO 8601 formatted string, representing the last time the resource was modified, helps invalidate caches. + * + * How to assign a priority: + * - `Indexes`: A resource index for directory nav is generally higher `0.8` to `1.0`, it's an anchor + * point if the model needs a map. + * - `Dynamic resource templates`: A resource template that contains dynamic content is generally lower `0.3` to `0.5`, + * it's a placeholder for a resource, and can generally shift. It can also be reattained by calling again. */ type McpResource = [ name: string, uriOrTemplate: string | ResourceTemplate, config: ResourceMetadata, handler: (...args: any[]) => any | Promise, - metadata?: McpResourceMetadata | undefined + metadata?: McpResourceMetadata | undefined, + _config?: { + shouldRegister?: (options: GlobalOptions) => boolean | Promise; + } ]; /** diff --git a/src/options.defaults.ts b/src/options.defaults.ts index e6f84186..089d9022 100644 --- a/src/options.defaults.ts +++ b/src/options.defaults.ts @@ -10,6 +10,9 @@ import { getNodeMajorVersion } from './options.helpers'; * @interface DefaultOptions * * @template TLogOptions The logging options type, defaulting to LoggingOptions. + * @property contextManagement - Strategy for managing agent context and response sizes, primarily within MCP tools. + * - 'false': Default standard text-heavy responses. + * - 'true': High-efficiency mode for MCP tools, using McpResource links. * @property contextPath - Current working directory. * @property contextUrl - Current working directory URL. * @property docsPaths - List of allowed local documentation directories handled by `docsPathSlug` @@ -49,6 +52,7 @@ import { getNodeMajorVersion } from './options.helpers'; * @property xhrFetch - XHR and Fetch options. */ interface DefaultOptions { + contextManagement: boolean; contextPath: string; contextUrl: string; docsPaths: string[]; @@ -131,7 +135,8 @@ interface LoggingOptions { * @interface MinMax * * @property urlString Minimum and maximum length for URL strings. - * @property toolSearches Minimum and maximum number of tool searches. + * @property resourceSearches Minimum and maximum number of resource results for searches. + * @property toolSearches Minimum and maximum number of tool results for searches. * @property inputStrings Minimum and maximum length for input strings. * @property docsToLoad Minimum and maximum number of docs to load. */ @@ -140,6 +145,10 @@ interface MinMax { min: number; max: number; } + resourceSearches: { + min: number; + max: number; + } toolSearches: { min: number; max: number; @@ -319,12 +328,19 @@ const HTTP_OPTIONS: HttpOptions = { /** * Minimum and maximum ranges for various options. + * + * @note For resourceSearches you still have to take into account that for every result + * there could be multiple resources. */ const MIN_MAX: MinMax = { urlString: { min: 11, max: 1500 }, + resourceSearches: { + min: 0, + max: 15 + }, toolSearches: { min: 0, max: 10 @@ -498,6 +514,7 @@ const PLUGIN_ISOLATION: DefaultOptions['pluginIsolation'][] = ['none', 'strict'] * @type {DefaultOptions} Default options object. */ const DEFAULT_OPTIONS: DefaultOptions = { + contextManagement: false, contextPath: (process.env.NODE_ENV === 'local' && '/') || resolve(process.cwd()), contextUrl: pathToFileURL((process.env.NODE_ENV === 'local' && '/') || resolve(process.cwd())).href, docsPaths: [], diff --git a/src/options.parser.ts b/src/options.parser.ts index 86d45587..cf79da8a 100644 --- a/src/options.parser.ts +++ b/src/options.parser.ts @@ -224,6 +224,9 @@ const parseCliOptions = ( } } break; + case '--context-management': + result.contextManagement = true; + break; } }; diff --git a/src/options.registry.ts b/src/options.registry.ts index 20ec3a9b..3a486c7a 100644 --- a/src/options.registry.ts +++ b/src/options.registry.ts @@ -1,4 +1,5 @@ import { type McpToolCreator, type McpResourceCreator } from './mcpSdk'; +import { searchPatternFlyTool } from './tool.searchPatternFly'; import { usePatternFlyDocsTool } from './tool.patternFlyDocs'; import { searchPatternFlyDocsTool } from './tool.searchPatternFlyDocs'; import { patternFlyComponentsIndexResource } from './resource.patternFlyComponentsIndex'; @@ -15,7 +16,8 @@ import { patternFlySchemasTemplateResource } from './resource.patternFlySchemasT */ const builtinTools: McpToolCreator[] = [ usePatternFlyDocsTool, - searchPatternFlyDocsTool + searchPatternFlyDocsTool, + searchPatternFlyTool ]; /** diff --git a/src/options.ts b/src/options.ts index 0f9e508e..749e0de6 100644 --- a/src/options.ts +++ b/src/options.ts @@ -86,7 +86,8 @@ const SET_OPTIONS = { docsPaths: defineOption({ cli: false })(), name: defineOption({ cli: false })(), toolModules: defineOption({ cli: true })(), - version: defineOption({ cli: false })() + version: defineOption({ cli: false })(), + contextManagement: defineOption({ cli: true, experimental: true })() } as const; /** diff --git a/src/resource.patternFlyComponentsIndex.ts b/src/resource.patternFlyComponentsIndex.ts index eddcbdab..c2ccaf80 100644 --- a/src/resource.patternFlyComponentsIndex.ts +++ b/src/resource.patternFlyComponentsIndex.ts @@ -37,7 +37,11 @@ const URI_DESCRIPTION = `Filter by PatternFly version and category. ${URI_TEMPLA const CONFIG = { title: 'PatternFly Components Index', description: `A list of all PatternFly component names available for documentation retrieval. ${URI_DESCRIPTION}`, - mimeType: 'text/markdown' + mimeType: 'text/markdown', + annotations: { + priority: 0.9, + audience: ['assistant' as const] + } }; /** diff --git a/src/resource.patternFlyContext.ts b/src/resource.patternFlyContext.ts index 79912c8c..48328127 100644 --- a/src/resource.patternFlyContext.ts +++ b/src/resource.patternFlyContext.ts @@ -18,7 +18,11 @@ const URI_TEMPLATE = 'patternfly://context'; const CONFIG = { title: 'PatternFly Design System Context', description: 'Information about the PatternFly design system and how to use this MCP server, including environment and troubleshooting information.', - mimeType: 'text/markdown' + mimeType: 'text/markdown', + annotations: { + priority: 0.5, + audience: ['assistant' as const] + } }; /** @@ -44,6 +48,7 @@ const resourceCallback = async (passedUri: URL, options = getOptions()) => { options.repoBugs && `- **Report bugs:** ${options.repoBugs}` ); + const availableToolFunctions = options.contextManagement ? 'search, list and access' : 'search, fetch and display'; const context = `PatternFly is an open-source design system for building consistent, accessible user interfaces. **What is PatternFly?** @@ -57,13 +62,14 @@ PatternFly provides React components, design guidelines, and development tools f **PatternFly MCP Server:** This MCP server provides tools and resources to access all PatternFly documentation resources ranging from design to development. -- **MCP tools:** Can be used to search, fetch and display available documentation resources. -- **MCP resources:** Can be used to list, filter and display available documentation resources. +- **MCP tools:** Can be used to ${availableToolFunctions} available documentation resources. +- **MCP resources:** Can be used to list, filter, and read available documentation resources. **Environment:** - **MCP Server Mode:** ${options.mode} - **MCP Server Version:** ${options.version || 'Unknown'} - **Node.js Major Version:** ${options.nodeVersion || 'Unknown'} +- **Context Management:** ${options.contextManagement} ${(troubleshooting && stringJoin.newline('**Troubleshooting:**', troubleshooting)) || ''} `; diff --git a/src/resource.patternFlyDocsIndex.ts b/src/resource.patternFlyDocsIndex.ts index ef4bf31c..80dfec1a 100644 --- a/src/resource.patternFlyDocsIndex.ts +++ b/src/resource.patternFlyDocsIndex.ts @@ -58,7 +58,11 @@ const URI_DESCRIPTION = `Filter by PatternFly version, category, and section. ${ const CONFIG = { title: 'PatternFly Documentation Index', description: `A list of PatternFly documentation links including accessibility, components, charts, development, writing, and AI guidance files. ${URI_DESCRIPTION}`, - mimeType: 'text/markdown' + mimeType: 'text/markdown', + annotations: { + priority: 1.0, + audience: ['assistant' as const] + } }; /** diff --git a/src/resource.patternFlyDocsTemplate.ts b/src/resource.patternFlyDocsTemplate.ts index 3fd230d0..b3b041e6 100644 --- a/src/resource.patternFlyDocsTemplate.ts +++ b/src/resource.patternFlyDocsTemplate.ts @@ -39,7 +39,11 @@ const URI_DESCRIPTION = `Filter by PatternFly version, category, and section. ${ const CONFIG = { title: 'PatternFly Documentation Page', description: `Retrieve specific PatternFly documentation by name or path. ${URI_DESCRIPTION}`, - mimeType: 'text/markdown' + mimeType: 'text/markdown', + annotations: { + priority: 0.4, + audience: ['assistant' as const] + } }; /** diff --git a/src/resource.patternFlySchemasIndex.ts b/src/resource.patternFlySchemasIndex.ts index 1ab28319..a33bb3e1 100644 --- a/src/resource.patternFlySchemasIndex.ts +++ b/src/resource.patternFlySchemasIndex.ts @@ -34,7 +34,11 @@ const URI_DESCRIPTION = `Filter by PatternFly version and category. ${URI_TEMPLA const CONFIG = { title: 'PatternFly Component Schemas Index', description: `A list of all PatternFly component names available for JSON Schema retrieval. ${URI_DESCRIPTION}`, - mimeType: 'text/markdown' + mimeType: 'text/markdown', + annotations: { + priority: 0.8, + audience: ['assistant' as const] + } }; /** diff --git a/src/resource.patternFlySchemasTemplate.ts b/src/resource.patternFlySchemasTemplate.ts index 57bbd751..11e5f35a 100644 --- a/src/resource.patternFlySchemasTemplate.ts +++ b/src/resource.patternFlySchemasTemplate.ts @@ -37,7 +37,11 @@ const URI_DESCRIPTION = `Filter by PatternFly version and category. ${URI_TEMPLA const CONFIG = { title: 'PatternFly Component Schema', description: `Retrieve the JSON Schema for a specific PatternFly component by name. ${URI_DESCRIPTION}`, - mimeType: 'application/json' + mimeType: 'application/json', + annotations: { + priority: 0.3, + audience: ['assistant' as const] + } }; /** diff --git a/src/server.ts b/src/server.ts index a637c509..a3adc48d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -117,7 +117,18 @@ interface ServerInstance { */ const registerServerResources = async (resources: McpResourceCreator[], server: McpServer, options = getOptions(), session = getSessionOptions()) => { for (const resourceCreator of resources) { - const [name, uri, config, callback, metadata] = resourceCreator(options); + const [name, uri, config, callback, metadata, _config] = resourceCreator(options); + + const shouldRegister = _config?.shouldRegister; + + if (shouldRegister) { + const status = await shouldRegister(options); + + if (!status) { + log.debug(`Skipping resource registration: ${name}`); + continue; + } + } try { registerResource(server, name, uri, config, (...args: unknown[]) => @@ -156,7 +167,17 @@ const registerServerResources = async (resources: McpResourceCreator[], server: */ const registerServerTools = async (tools: McpToolCreator[], server: McpServer, options = getOptions(), session = getSessionOptions()) => { for (const toolCreator of tools) { - const [name, schema, callback] = toolCreator(options); + const [name, schema, callback, _config] = toolCreator(options); + const shouldRegister = _config?.shouldRegister; + + if (shouldRegister) { + const status = await shouldRegister(options); + + if (!status) { + log.debug(`Skipping tool registration: ${name}`); + continue; + } + } // Do NOT normalize schemas here. This is by design and is a fallback check for malformed schemas. const isZod = isZodSchema(schema?.inputSchema) || isZodRawShape(schema?.inputSchema); diff --git a/src/tool.patternFlyDocs.ts b/src/tool.patternFlyDocs.ts index 687c6609..62e74a92 100644 --- a/src/tool.patternFlyDocs.ts +++ b/src/tool.patternFlyDocs.ts @@ -240,9 +240,18 @@ const usePatternFlyDocsTool = (options = getOptions()): McpTool => { .optional().describe('The name of a PatternFly component or resource to fetch documentation for (e.g., "Button", "Table", "Writing")'), version: z.enum(options.patternflyOptions.availableSearchVersions) .optional().describe(`Filter results by a specific PatternFly version (e.g. ${options.patternflyOptions.availableSearchVersions.map(value => `"${value}"`).join(', ')})`) + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true } }, - callback + callback, + { + shouldRegister: opts => opts.contextManagement === false || opts.contextManagement === undefined + } ]; }; diff --git a/src/tool.searchPatternFly.ts b/src/tool.searchPatternFly.ts new file mode 100644 index 00000000..c3e97d28 --- /dev/null +++ b/src/tool.searchPatternFly.ts @@ -0,0 +1,182 @@ +import { ErrorCode } from '@modelcontextprotocol/sdk/types.js'; +import { z } from 'zod'; +import { type McpTool } from './mcpSdk'; +import { stringJoin } from './server.helpers'; +import { assertInput, assertInputStringLength, assertInputStringNumberEnumLike } from './server.assertions'; +import { getOptions } from './options.context'; +import { searchPatternFly } from './patternFly.search'; +import { getPatternFlyMcpResources } from './patternFly.getResources'; +import { normalizeEnumeratedPatternFlyVersion } from './patternFly.helpers'; +import { findClosest } from './server.search'; + +/** + * searchPatternFly tool function + * + * Searches for PatternFly resources using fuzzy search. + * Returns MCP Resource Links when contextManagement: 'token-saver' is active. + * + * @note Review not filtering out resources without a path. These resources could be + * inlined or handled with the upcoming on-demand session resource loader. + * + * @param options - Optional configuration options (defaults to OPTIONS) + * @returns MCP tool tuple [name, schema, callback] + */ +const searchPatternFlyTool = (options = getOptions()): McpTool => { + const callback = async (args: any) => { + const { query: searchQuery, version } = args; + const isVersion = typeof version === 'string' && version.length > 0; + + assertInputStringLength(searchQuery, { + ...options.minMax.inputStrings, + inputDisplayName: 'searchQuery' + }); + + if (isVersion) { + assertInputStringLength(version, { + max: options.minMax.inputStrings.max, + min: 2, + inputDisplayName: 'version' + }); + + assertInputStringNumberEnumLike(version, options.patternflyOptions.availableSearchVersions, { + inputDisplayName: 'version' + }); + } + + const { latestVersion, keywordsIndex } = await getPatternFlyMcpResources.memo(); + const normalizedVersion = await normalizeEnumeratedPatternFlyVersion(version); + const updatedVersion = normalizedVersion || latestVersion; + + const { isSearchWildCardAll, exactMatches, remainingMatches, searchResults, totalPotentialMatches } = await searchPatternFly.memo( + searchQuery, + { version: updatedVersion }, + { allowWildCardAll: true, maxResults: options.minMax.resourceSearches.max } + ); + + assertInput( + !isSearchWildCardAll || (isSearchWildCardAll && searchResults.length > 0), + stringJoin.newline( + `Internal Search Error: The server failed to retrieve PatternFly resources for query "${searchQuery}"`, + 'Ensure documentation resources are loaded or restart the server.' + ), + ErrorCode.InternalError + ); + + if (!isSearchWildCardAll && searchResults.length === 0) { + const suggestion = findClosest.memo(searchQuery, keywordsIndex.reverse(), { maxDistance: 5 }); + const hint = suggestion ? `Try a search for "${suggestion}".` : `Try a broader search.`; + + return { + content: [{ + type: 'text', + text: stringJoin.newlineFiltered( + `No PatternFly resources found matching "${searchQuery}". ${hint}` + ) + }] + }; + } + + // Default to parsing all remainingMatches + let parseResults = remainingMatches; + + // Focus the result set. If there are exact matches, use those. + if (isSearchWildCardAll || exactMatches.length > 0) { + parseResults = exactMatches; + + // Focus the result set. If there aren't any exactMatches, but we have "distance 1" matches, use those. + } else if (searchResults.some(result => result.distance === 1)) { + parseResults = searchResults.filter(result => result.distance === 1); + } + + const results = new Map>(); + + parseResults + .map(result => result.entries) + .flat() + .filter(entry => entry.path) + .forEach(entry => { + if (entry.uriId && !results.has(entry.uriId)) { + results.set(entry.uriId, { + type: 'resource_link', + uri: entry.uriId, + name: `${entry.displayName} - ${entry.displayCategory} (${entry.version})`, + description: entry.description, + mimeType: 'text/markdown' + }); + } + + if (entry.uriSchemasId && !results.has(entry.uriSchemasId)) { + results.set(entry.uriSchemasId, { + type: 'resource_link', + uri: entry.uriSchemasId, + name: `${entry.displayName} - JSON Schemas (${entry.version})`, + description: `${entry.displayName} component JSON schemas.`, + mimeType: 'application/json' + }); + } + }); + + const resultValues = Array.from(results.values()); + + const summaryTitlePatternFly = updatedVersion + ? `Search results for PatternFly version "${updatedVersion}" and` + : `Search results for`; + + let summaryTitle = stringJoin.newline( + `# ${summaryTitlePatternFly} "${searchQuery}".`, + `Found ${resultValues.length} related ${resultValues.length === 1 ? 'resource' : 'resources'}. Use the attached resources to access and read full content.` + ); + + if (isSearchWildCardAll) { + summaryTitle = stringJoin.newline( + `# ${summaryTitlePatternFly} "all" resources.`, + `Only showing ${resultValues.length} ${resultValues.length === 1 ? 'resource' : 'resources'} out of ${totalPotentialMatches} potential matches. Use a more specific query.` + ); + } else if (exactMatches.length > 0) { + summaryTitle = stringJoin.newline( + `# ${summaryTitlePatternFly} "${searchQuery}".`, + `Found ${resultValues.length} ${resultValues.length === 1 ? 'resource' : 'resources'}. Use the attached resources to access and read full content.` + ); + } + + return { + content: [ + { + type: 'text', + text: summaryTitle + }, + ...resultValues + ] + }; + }; + + return [ + 'searchPatternFly', + { + description: `Search PatternFly components, documentation, guidelines, and resource links by keywords or '*' for all.`, + inputSchema: { + query: z.string() + .min(options.minMax.inputStrings.min) + .max(options.minMax.inputStrings.max) + .describe('Case-insensitive, full or partial keyword query (e.g., "button", "react", "*")'), + version: z.enum(options.patternflyOptions.availableSearchVersions) + .optional() + .describe(`Filter results by a specific PatternFly version (e.g. ${options.patternflyOptions.availableSearchVersions.map(value => `"${value}"`).join(', ')})`) + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } + }, + callback, + { + shouldRegister: opts => opts.contextManagement === true + } + ]; +}; + +searchPatternFlyTool.toolName = 'searchPatternFly'; + +export { searchPatternFlyTool }; diff --git a/src/tool.searchPatternFlyDocs.ts b/src/tool.searchPatternFlyDocs.ts index a913ccdc..ccecc70d 100644 --- a/src/tool.searchPatternFlyDocs.ts +++ b/src/tool.searchPatternFlyDocs.ts @@ -168,9 +168,18 @@ const searchPatternFlyDocsTool = (options = getOptions()): McpTool => { version: z.enum(options.patternflyOptions.availableSearchVersions) .optional() .describe(`Filter results by a specific PatternFly version (e.g. ${options.patternflyOptions.availableSearchVersions.map(value => `"${value}"`).join(', ')})`) + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true } }, - callback + callback, + { + shouldRegister: opts => opts.contextManagement === false || opts.contextManagement === undefined + } ]; }; diff --git a/tests/e2e/__snapshots__/stdioTransport.test.ts.snap b/tests/e2e/__snapshots__/stdioTransport.test.ts.snap index 098cc273..ab3f573a 100644 --- a/tests/e2e/__snapshots__/stdioTransport.test.ts.snap +++ b/tests/e2e/__snapshots__/stdioTransport.test.ts.snap @@ -181,6 +181,10 @@ exports[`Logging should allow setting logging options, stderr 1`] = ` ] `; +exports[`Logging should allow setting logging options, with experimental flag default 1`] = `[]`; + +exports[`Logging should allow setting logging options, with experimental flag set 1`] = `[]`; + exports[`Logging should allow setting logging options, with log level filtering 1`] = `[]`; exports[`Logging should allow setting logging options, with mcp protocol 1`] = ` diff --git a/tests/e2e/httpTransport.test.ts b/tests/e2e/httpTransport.test.ts index facb63ac..bb16970f 100644 --- a/tests/e2e/httpTransport.test.ts +++ b/tests/e2e/httpTransport.test.ts @@ -523,3 +523,52 @@ describe('Inline tools, HTTP transport', () => { await CLIENT.close(); }); }); + +describe('token-saver mode, HTTP transport', () => { + let CLIENT: HttpTransportClient | undefined; + + beforeAll(async () => { + CLIENT = await startServer({ + isHttp: true, + experimentalContextManagement: true + }); + }); + + afterAll(async () => { + if (CLIENT) { + await CLIENT.close(); + } + }); + + it('should only expose searchPatternFly tool', async () => { + const response = await CLIENT?.send({ + method: 'tools/list', + params: {} + }); + const tools = response?.result?.tools || []; + const toolNames = tools.map((tool: any) => tool.name); + + expect(toolNames).toEqual(['searchPatternFly']); + }); + + it('should return McpResource links from searchPatternFly', async () => { + const response = await CLIENT?.send({ + method: 'tools/call', + params: { + name: 'searchPatternFly', + arguments: { + query: 'Button' + } + } + }); + + const [summary, ...resources] = response?.result?.content || []; + + expect(summary.type).toBe('text'); + + resources.forEach((item: any) => { + expect(item.type).toBe('resource_link'); + expect(item.uri).toMatch(/^patternfly:\/\/(docs|schemas|components)\//); + }); + }); +}); diff --git a/tests/e2e/stdioTransport.test.ts b/tests/e2e/stdioTransport.test.ts index 7e4d0ddd..f27f7e5a 100644 --- a/tests/e2e/stdioTransport.test.ts +++ b/tests/e2e/stdioTransport.test.ts @@ -476,6 +476,14 @@ describe('Logging', () => { { description: 'with mcp protocol', args: ['--log-protocol'] + }, + { + description: 'with experimental flag default', + args: ['--experimental-context-management', 'default'] + }, + { + description: 'with experimental flag set', + args: ['--experimental-context-management', 'token-saver'] } ])('should allow setting logging options, $description', async ({ args }) => { const serverArgs = [...args]; @@ -556,3 +564,54 @@ describe('Tools', () => { expect(resp.result.isError).toBeUndefined(); }); }); + +describe('token-saver mode', () => { + let CLIENT: StdioTransportClient; + + beforeAll(async () => { + CLIENT = await startServer({ + args: ['--experimental-context-management', 'token-saver'] + }); + }); + + afterAll(async () => { + if (CLIENT) { + await CLIENT.close(); + } + }); + + it('should only expose searchPatternFly tool', async () => { + const response = await CLIENT.send({ + method: 'tools/list', + params: {} + }); + const tools = response?.result?.tools || []; + const toolNames = tools.map((tool: any) => tool.name); + + expect(toolNames).toEqual(['searchPatternFly']); + }); + + it('should return McpResource links from searchPatternFly', async () => { + const response = await CLIENT.send({ + method: 'tools/call', + params: { + name: 'searchPatternFly', + arguments: { + query: 'Button' + } + } + }); + + const [summary, ...resources] = response?.result?.content || []; + + console.warn(summary); + console.warn(resources); + + expect(summary.type).toBe('text'); + + resources.forEach((item: any) => { + expect(item.type).toBe('resource_link'); + expect(item.uri).toMatch(/^patternfly:\/\/(docs|schemas|components)\//); + }); + }); +}); diff --git a/tests/e2e/utils/httpTransportClient.ts b/tests/e2e/utils/httpTransportClient.ts index 05ef6179..d4bcb0a1 100644 --- a/tests/e2e/utils/httpTransportClient.ts +++ b/tests/e2e/utils/httpTransportClient.ts @@ -25,6 +25,7 @@ export type StartHttpServerOptions = { logging?: Partial & { level?: LoggingLevel }; toolModules?: PfMcpOptions['toolModules']; modeOptions?: PfMcpOptions['modeOptions']; + experimentalContextManagement?: PfMcpOptions['experimentalContextManagement']; }; export type StartHttpServerSettings = PfMcpSettings;