From b408de1bf71894ba042af6e28c6bb2f3aad8e886 Mon Sep 17 00:00:00 2001 From: arafatkatze Date: Sat, 11 Jan 2025 21:52:41 +0400 Subject: [PATCH] chore: update pnpm lockfile for modelcontextprotocoltools dependencies --- lib/shared/src/context/openctx/api.ts | 1 + lib/shared/src/index.ts | 1 + package.json | 2 +- vscode/package.json | 10 ++ vscode/src/chat/agentic/CodyTool.ts | 102 +++++++++++++- vscode/src/chat/agentic/CodyToolProvider.ts | 131 +++++++++++++++--- vscode/src/chat/context/chatContext.ts | 2 +- vscode/src/context/openctx.ts | 10 ++ .../context/openctx/modelContextProvider.ts | 36 +++++ 9 files changed, 269 insertions(+), 26 deletions(-) create mode 100644 vscode/src/context/openctx/modelContextProvider.ts diff --git a/lib/shared/src/context/openctx/api.ts b/lib/shared/src/context/openctx/api.ts index 4f915c693185..9d9da44d20b3 100644 --- a/lib/shared/src/context/openctx/api.ts +++ b/lib/shared/src/context/openctx/api.ts @@ -32,3 +32,4 @@ export const REMOTE_DIRECTORY_PROVIDER_URI = 'internal-remote-directory-search' export const WEB_PROVIDER_URI = 'internal-web-provider' export const GIT_OPENCTX_PROVIDER_URI = 'internal-git-openctx-provider' export const CODE_SEARCH_PROVIDER_URI = 'internal-code-search-provider' +export const MODEL_CONTEXT_PROVIDER_URI = 'internal-model-context-provider' diff --git a/lib/shared/src/index.ts b/lib/shared/src/index.ts index 09ea3037de45..53bd92cf7970 100644 --- a/lib/shared/src/index.ts +++ b/lib/shared/src/index.ts @@ -359,6 +359,7 @@ export { WEB_PROVIDER_URI, GIT_OPENCTX_PROVIDER_URI, CODE_SEARCH_PROVIDER_URI, + MODEL_CONTEXT_PROVIDER_URI, } from './context/openctx/api' export * from './context/openctx/context' export * from './lexicalEditor/editorState' diff --git a/package.json b/package.json index e702b6a6fa21..1e0762242d77 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "update-agent-recordings": "pnpm build && CODY_KEEP_UNUSED_RECORDINGS=false CODY_RECORD_IF_MISSING=true vitest agent/src", "update-agent-recordings-windows": "PowerShell -ExecutionPolicy Bypass -Command \"pnpm build; if ($?) { $Env:CODY_KEEP_UNUSED_RECORDINGS='false'; $Env:CODY_RECORD_IF_MISSING='true'; vitest agent/src }\"", "update-rewrite-recordings": "rm -rf recordings && CODY_RECORD_IF_MISSING=true CODY_RECORDING_MODE=record vitest vscode/src/local-context/rewrite-keyword-query.test.ts", - "openctx:link": "cd ../openctx && pnpm -C lib/client link --global && pnpm -C lib/schema link --global && pnpm -C lib/protocol link --global && pnpm -C client/vscode-lib link --global && cd ../cody && pnpm link --global @openctx/client && pnpm link --global @openctx/schema && pnpm link --global @openctx/protocol && cd vscode && pnpm link --global @openctx/vscode-lib", + "openctx:link": "cd ../openctx && pnpm -C lib/client link --global && pnpm -C lib/schema link --global && pnpm -C lib/protocol link --global && pnpm -C client/vscode-lib link --global && pnpm -C provider/modelcontextprotocoltools link --global && cd ../cody && pnpm link --global @openctx/client && pnpm link --global @openctx/schema && pnpm link --global @openctx/protocol && pnpm link --global @openctx/provider-modelcontextprotocoltools && cd vscode && pnpm link --global @openctx/vscode-lib", "openctx:unlink": "pnpm unlink --global @openctx/client && pnpm unlink --global @openctx/schema && pnpm unlink --global @openctx/protocol && cd vscode && pnpm unlink --global @openctx/vscode-lib", "vsce-version-bump": "pnpm -C vscode version-bump:minor" }, diff --git a/vscode/package.json b/vscode/package.json index a2046020b1a1..598f3c9e5554 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -1244,6 +1244,16 @@ "markdownDescription": "OpenCtx providers configuration.", "default": {} }, + "openctx.mcp.enable": { + "type": "boolean", + "markdownDescription": "Enable OpenCtx providers for Cody.", + "default": true + }, + "openctx.mcp.uri": { + "type": "string", + "markdownDescription": "URI for the MCP provider tools in OpenCtx.", + "default": "" + }, "cody.internal.unstable": { "order": 999, "type": "boolean", diff --git a/vscode/src/chat/agentic/CodyTool.ts b/vscode/src/chat/agentic/CodyTool.ts index d830846a071f..a03f4d2be841 100644 --- a/vscode/src/chat/agentic/CodyTool.ts +++ b/vscode/src/chat/agentic/CodyTool.ts @@ -4,6 +4,8 @@ import { ContextItemSource, type ContextItemWithContent, type ContextMentionProviderMetadata, + MODEL_CONTEXT_PROVIDER_URI, + type MentionQuery, PromptString, firstValueFrom, logDebug, @@ -15,6 +17,7 @@ import { import { URI } from 'vscode-uri' import { getContextFromRelativePath } from '../../commands/context/file-path' import { getContextFileFromShell } from '../../commands/context/shell' +import type { OpenCtxProvider } from '../../context/openctx/types' import { type ContextRetriever, toStructuredMentions } from '../chat-view/ContextRetriever' import { getChatContextItemsForMention } from '../context/chatContext' import { getCorpusContextItemsForEditorState } from '../initialContext' @@ -94,7 +97,7 @@ export abstract class CodyTool { /** * Resets the raw text input stream. */ - private reset(): void { + public reset(): void { this.unprocessedText = '' } /** @@ -148,7 +151,7 @@ class CliTool extends CodyTool { }, prompt: { instruction: ps`Reject all unsafe and harmful commands with tags. Execute safe command for its output with tags`, - placeholder: ps`SAFE_COMMAND`, + placeholder: PromptString.unsafe_fromUserQuery('INPUT'), examples: [ ps`Get output for git diff: \`git diff\``, ps`List files in a directory: \`ls -l\``, @@ -257,6 +260,56 @@ class SearchTool extends CodyTool { } } +export class ModelContextProviderTool extends CodyTool { + constructor( + config: CodyToolConfig, + private modelContextProvider: OpenCtxProvider, + private toolName: string + ) { + super(config) + } + + public parse(): string[] { + try { + JSON.parse(this.unprocessedText) + } catch { + return [] + } + const unparsedText = this.unprocessedText + this.reset() + return [unparsedText] + } + + public async execute(span: Span, queries: string[]): Promise { + span.addEvent('executeModelContextProviderTool') + + try { + const rawItems = + (await this.modelContextProvider.items?.( + { mention: { title: this.toolName, data: JSON.parse('{}'), uri: '' } }, + {} + )) ?? [] + + return rawItems.map(item => ({ + type: 'openctx', + provider: 'openctx', + title: item.title, + uri: URI.parse(''), + providerUri: MODEL_CONTEXT_PROVIDER_URI, + content: item.ai?.content || '', + mention: { + uri: '', + data: item.ai, + description: item.ai?.content, + }, + })) + } catch (error) { + console.error('ModelContextProviderTool execution failed:', error) + return [] + } + } +} + /** * Tool for interacting with OpenCtx providers and retrieving context items. */ @@ -268,6 +321,23 @@ export class OpenCtxTool extends CodyTool { super(config) } + parse(): string[] { + if (this.provider.id === 'internal-model-context-provider') { + return [this.unprocessedText] + } + return super.parse() + } + + parseMCPMentionQuery( + query: string, + idObject: Pick + ): MentionQuery { + return { + provider: idObject.id, + text: query, + } + } + async execute(span: Span, queries: string[]): Promise { span.addEvent('executeOpenCtxTool') const openCtxClient = openCtx.controller @@ -279,7 +349,35 @@ export class OpenCtxTool extends CodyTool { try { // TODO: Investigate if we can batch queries for better performance. // For example, would it cause issues if we fire 10 requests to a OpenCtx provider for fetching Linear? + const toolName = this.config.title + + console.log('toolName', toolName) + for (const query of queries) { + if (this.provider.id === 'internal-model-context-provider') { + const mcpResults = await openCtxClient.items( + { mention: { uri: '', title: this.config.title, data: JSON.parse(query) } }, + { providerUri: MODEL_CONTEXT_PROVIDER_URI } + ) + console.log(mcpResults) + const itemsWithContent = mcpResults.map(item => ({ + type: 'openctx' as const, + title: item.title || '', + uri: URI.parse(''), + providerUri: MODEL_CONTEXT_PROVIDER_URI, + content: item.ai?.content || '', + provider: 'openctx' as const, + source: ContextItemSource.Agentic, + mention: { + uri: '', + data: item.ai, + description: item.ai?.content, + }, + })) + results.push(...itemsWithContent) + continue + } + const mention = parseMentionQuery(query, idObject) // First get the items without content const openCtxItems = await getChatContextItemsForMention({ mentionQuery: mention }) diff --git a/vscode/src/chat/agentic/CodyToolProvider.ts b/vscode/src/chat/agentic/CodyToolProvider.ts index 8aa5edf32e49..41f8e6e148e0 100644 --- a/vscode/src/chat/agentic/CodyToolProvider.ts +++ b/vscode/src/chat/agentic/CodyToolProvider.ts @@ -91,19 +91,63 @@ class ToolFactory { .filter(isDefined) } - public createOpenCtxTools(providers: ContextMentionProviderMetadata[]): CodyTool[] { - return providers - .map(provider => { + public async createOpenCtxTools(providers: ContextMentionProviderMetadata[]): Promise { + const tools: CodyTool[] = [] + + for (const provider of providers) { + if (provider.id === 'internal-model-context-provider') { + // For MCP providers, get available tools through the mentions() function + // get the Vscode Regex Here for query + const mcpTools = + (await openCtx.controller?.mentions( + { query: 'echo' }, + { providerUri: provider.id } + )) ?? [] + + for (const mcpTool of mcpTools) { + const toolName = this.generateToolName({ + ...provider, + title: mcpTool.title ?? provider.title, + }) + const config = this.createModelContextConfig( + { + title: mcpTool.title ?? '', + description: mcpTool.description ?? '', + data: mcpTool.data, + }, + toolName + ) + + this.register({ + name: toolName, + ...config, + createInstance: cfg => new OpenCtxTool(provider, cfg), + }) + + const tool = this.createTool(toolName) + if (tool) { + tools.push(tool) + } + } + } else { + // For regular providers, create a single tool as before const toolName = this.generateToolName(provider) const config = this.getToolConfig(provider) + this.register({ name: toolName, ...config, createInstance: cfg => new OpenCtxTool(provider, cfg), }) - return this.createTool(toolName) - }) - .filter(isDefined) + + const tool = this.createTool(toolName) + if (tool) { + tools.push(tool) + } + } + } + + return tools } private generateToolName(provider: ContextMentionProviderMetadata): string { @@ -124,20 +168,57 @@ class ToolFactory { const defaultConfig = Object.entries(OPENCTX_TOOL_CONFIG).find( c => provider.id.toLowerCase().includes(c[0]) || provider.title.toLowerCase().includes(c[0]) ) - return ( - defaultConfig?.[1] ?? { - title: provider.title, - tags: { - tag: PromptString.unsafe_fromUserQuery(this.generateToolName(provider)), - subTag: ps`get`, - }, - prompt: { - instruction: PromptString.unsafe_fromUserQuery(provider.queryLabel), - placeholder: ps`QUERY`, - examples: [], - }, - } - ) + if (defaultConfig) { + return defaultConfig[1] + } + return { + title: provider.title, + tags: { + tag: PromptString.unsafe_fromUserQuery(this.generateToolName(provider)), + subTag: ps`get`, + }, + prompt: { + instruction: PromptString.unsafe_fromUserQuery(provider.queryLabel), + placeholder: ps`QUERY`, + examples: [], + }, + } + } + // TODO: Handles this in getToolConfig instead of + // having a separate function specific to model context protocol + private createModelContextConfig( + mention: { + title: string + description: string + data?: any + }, + tagName: string + ): CodyToolConfig { + // Extract schema properties for better instruction formatting + const schemaProperties = mention.data?.properties || {} + + return { + title: mention.title, + tags: { + tag: PromptString.unsafe_fromUserQuery(tagName), + subTag: ps`QUERY`, + }, + prompt: { + instruction: PromptString.unsafe_fromUserQuery( + `Use ${mention.title} to ${mention.description || 'retrieve context'}. ` + + `Input must follow this schema::\n${JSON.stringify(schemaProperties, null, 2)}` + + 'Ensure all required properties are provided and types match the schema.' + ), + placeholder: PromptString.unsafe_fromUserQuery('INPUT'), + examples: [ + PromptString.unsafe_fromUserQuery( + `To use ${mention.title} with valid schema: \`<${tagName}>${JSON.stringify({ + message: mention.data?.properties || 'example input', + })}\`` + ), + ], + }, + } } } @@ -185,8 +266,14 @@ export class CodyToolProvider { if (provider && !CodyToolProvider.openCtxSubscription && openCtx.controller) { CodyToolProvider.openCtxSubscription = openCtx.controller .metaChanges({}, {}) - .pipe(map(providers => providers.filter(p => !!p.mentions).map(openCtxProviderMetadata))) - .subscribe(providerMeta => provider.factory.createOpenCtxTools(providerMeta)) + .pipe( + map(providers => + providers.filter(p => !!p.mentions).map(p => openCtxProviderMetadata(p)) + ) + ) + .subscribe(async providers => { + provider.factory.createOpenCtxTools(providers) + }) } } diff --git a/vscode/src/chat/context/chatContext.ts b/vscode/src/chat/context/chatContext.ts index 0dbf88a04d7d..ce13f7340f7f 100644 --- a/vscode/src/chat/context/chatContext.ts +++ b/vscode/src/chat/context/chatContext.ts @@ -156,7 +156,7 @@ export async function getChatContextItemsForMention( if (!openCtx.controller) { return [] } - + // For MCP provider, we need to call items() with the mention const items = await openCtx.controller.mentions( { query: mentionQuery.text, diff --git a/vscode/src/context/openctx.ts b/vscode/src/context/openctx.ts index 54a2c4e6d86d..595421421b10 100644 --- a/vscode/src/context/openctx.ts +++ b/vscode/src/context/openctx.ts @@ -4,6 +4,7 @@ import { type ClientConfiguration, FeatureFlag, GIT_OPENCTX_PROVIDER_URI, + MODEL_CONTEXT_PROVIDER_URI, WEB_PROVIDER_URI, authStatus, clientCapabilities, @@ -35,6 +36,7 @@ import { logDebug } from '../output-channel-logger' import { createCodeSearchProvider } from './openctx/codeSearch' import { gitMentionsProvider } from './openctx/git' import LinearIssuesProvider from './openctx/linear-issues' +import { createModelContextProvider } from './openctx/modelContextProvider' import RemoteDirectoryProvider, { createRemoteDirectoryProvider } from './openctx/remoteDirectorySearch' import RemoteFileProvider, { createRemoteFileProvider } from './openctx/remoteFileSearch' import RemoteRepositorySearch, { createRemoteRepositoryProvider } from './openctx/remoteRepositorySearch' @@ -185,6 +187,14 @@ export function getOpenCtxProviders( providerUri: CODE_SEARCH_PROVIDER_URI, }) } + // enable MCP provider + providers.push({ + settings: true, + provider: createModelContextProvider( + 'file:///Users/arafatkhan/Desktop/servers/src/everything/dist/index.js' + ), + providerUri: MODEL_CONTEXT_PROVIDER_URI, + }) return providers } diff --git a/vscode/src/context/openctx/modelContextProvider.ts b/vscode/src/context/openctx/modelContextProvider.ts new file mode 100644 index 000000000000..5bd2218badd1 --- /dev/null +++ b/vscode/src/context/openctx/modelContextProvider.ts @@ -0,0 +1,36 @@ +import proxy from '@openctx/provider-modelcontextprotocoltools' + +import { MODEL_CONTEXT_PROVIDER_URI } from '@sourcegraph/cody-shared' +import type { OpenCtxProvider } from './types' + +export function createModelContextProvider(modelContextProviderToolsURI: string): OpenCtxProvider { + return { + providerUri: MODEL_CONTEXT_PROVIDER_URI, + + async meta() { + const client = await proxy.meta!( + {}, + { + 'mcp.provider.uri': modelContextProviderToolsURI, + 'mcp.provider.args': [], + } + ) + return { + name: client.name, + mentions: { label: client.name ?? 'Select a tool' }, + } + }, + + // returns a list of available tools in MCP + async mentions({ query }) { + const items = await proxy.mentions!({ query: query }, {}) + return items + }, + + // returns the result of calling a speciic tool of MCP + async items({ mention }) { + const items = await proxy.items!({ mention }, {}) + return items + }, + } +}