diff --git a/core/context/mcp/index.ts b/core/context/mcp/index.ts index 19de5434a3..a19dad9ab4 100644 --- a/core/context/mcp/index.ts +++ b/core/context/mcp/index.ts @@ -9,6 +9,7 @@ import { ContinueConfig, MCPOptions, SlashCommand, Tool } from "../.."; import { constructMcpSlashCommand } from "../../commands/slash/mcp"; import { encodeMCPToolUri } from "../../tools/callTool"; import MCPContextProvider from "../providers/MCPContextProvider"; +import { createMCPTemplateContextProviderClass } from "../providers/MCPTemplateContextProvider"; export class MCPManagerSingleton { private static instance: MCPManagerSingleton; @@ -167,6 +168,42 @@ class MCPConnection { mcpId, }), ); + + const templates = await this.client.listResourceTemplates({}, { signal }); + const templateContextProviders = templates.resourceTemplates.map( + (resource: any) => { + const matches = resource.uriTemplate.matchAll( + /{([^}]+)}/g, + ) as IterableIterator; + const params = Array.from(matches, (m) => m[1]); + if (params.length === 0) { + throw new Error( + `No parameters found in resource template ${resource.name}`, + ); + } + + if (params.length > 1) { + throw new Error( + `Cant use resource template ${resource.name} with more than one parameter. This is a limitation of the current implementation.`, + ); + } + + const parameter = params[0]; + /// create temporary context provider for each mcp resource template + /// For now this is necessary as we dont have an option to get from a submenu to a query input + const Provider = createMCPTemplateContextProviderClass({ + // combine the MCP server name and resource name into a unique display name + name: `${mcpId}.${resource.name}`, + description: resource.description, + }); + return new Provider({ + mcpId, + parameter, + uri: resource.uriTemplate, + }); + }, + ); + config.contextProviders.push(...templateContextProviders); } // Tools <—> Tools diff --git a/core/context/providers/MCPTemplateContextProvider.ts b/core/context/providers/MCPTemplateContextProvider.ts new file mode 100644 index 0000000000..301ed0297c --- /dev/null +++ b/core/context/providers/MCPTemplateContextProvider.ts @@ -0,0 +1,74 @@ +import { ContextItem, ContextProviderExtras } from "../../index.js"; +import { BaseContextProvider } from "../index.js"; +import { MCPManagerSingleton } from "../mcp"; + +interface MCPTemplateContextProviderOptions { + name: string; + description: string; +} + +class MCPTemplateContextProvider extends BaseContextProvider { + private readonly _mcpId: string; + private readonly _parameter: string; + private readonly _uri: string; + + constructor(options: { mcpId: string; parameter: string; uri: string }) { + super(options); + this._mcpId = options.mcpId; + this._parameter = options.parameter; + this._uri = options.uri; + } + + async getContextItems( + query: string, + extras: ContextProviderExtras, + ): Promise { + const connection = MCPManagerSingleton.getInstance().getConnection( + this._mcpId, + ); + if (!connection) { + throw new Error(`No MCP connection found for ${this._mcpId}`); + } + + const uri = this._uri.replace(`{${this._parameter}}`, query); + + const { contents } = await connection.client.readResource({ uri }); + + return await Promise.all( + contents.map(async (resource) => { + const content = resource.text; + if (typeof content !== "string") { + throw new Error( + "Continue currently only supports text resources from MCP", + ); + } + + return { + name: resource.uri, + description: resource.uri, + content, + uri: { + type: "url", + value: resource.uri, + }, + }; + }), + ); + } +} + +// Factory to create subclasses with custom static descriptions +export function createMCPTemplateContextProviderClass( + options: MCPTemplateContextProviderOptions, +): typeof MCPTemplateContextProvider { + return class extends MCPTemplateContextProvider { + static description = { + title: options.name, + displayTitle: options.name, + description: options.description, + type: "query", + }; + }; +} + +export default MCPTemplateContextProvider;