From 9777be2cd3c8ef8b6cf0f9f2e4d872ec39c53b04 Mon Sep 17 00:00:00 2001 From: Alexander Smirnov Date: Thu, 27 Feb 2025 21:43:32 -0800 Subject: [PATCH 1/3] process prompt from mcp server with handle of all user history variations. Preserves all content resources for llm call --- core/commands/slash/mcp.ts | 174 +++++++++++++++++-- core/context/providers/MCPContextProvider.ts | 8 +- core/tools/callTool.ts | 8 + 3 files changed, 172 insertions(+), 18 deletions(-) diff --git a/core/commands/slash/mcp.ts b/core/commands/slash/mcp.ts index a9fa332bbf..7908f20e80 100644 --- a/core/commands/slash/mcp.ts +++ b/core/commands/slash/mcp.ts @@ -1,7 +1,120 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { ChatMessage, SlashCommand } from "../../index.js"; +import { ChatMessage, MessagePart, SlashCommand, UserChatMessage } from "../../index.js"; import { renderChatMessage } from "../../util/messageContent.js"; + +/** + * Function substitutes the content of the message with the prompt + * Find the last text message that contains the input in content, + * and removes all the ocurrences of input in content of the found part. + * Keep the part in place with new content + * Insert prompt messages before the found message, or at the start + * of array if no message is found. + * + * @param content original content + * @param input string to be replaced + * @param prompt replacement content + * @returns new content with inserted prompt + */ +function substituteContent(content: MessagePart[], input: string, prompt: MessagePart[]): MessagePart[] { + // Create a copy of the original content to avoid modifying the input + const newContent = [...content]; + + // Find the last text message part that contains the input + const foundIndex = newContent.findLastIndex((part) => part.type === "text" && part.text.includes(input)); + + if (foundIndex !== -1) { + // Found a part containing the input + const part = newContent[foundIndex]; + if (part.type === "text") { + // Remove all occurrences of input from the text + const updatedText = part.text.replace(input,''); + // Update the part with the new text + newContent[foundIndex] = { ...part, text: updatedText }; + } + + // Insert prompt messages before the found message + newContent.splice(foundIndex, 0, ...prompt); + } else { + // No message found containing the input, insert at the start + newContent.unshift(...prompt); + } + + return newContent; +} + +/** + * Function substitutes prompt messages into the history. + * The action is cumbersome, because history may contain multiple + * messages, and prompt may contain multiple messages. And prompt should be + * inserted before the last user message in history, replacing original slash command. + * Also, each message may be just a text, or an array of MessagePart with text or image. + * Finds the last user message in the chat history and substitutes its content + * using the substituteContent function. Handles both string and MessagePart[] content types. + * + * @param messages Array of chat messages + * @param input String to be replaced in the last user message + * @param prompt Replacement content to be inserted + * @returns New array of messages with the substitution applied + */ +function substituteLastUserMessage(messages: ChatMessage[], input: string, prompt: ChatMessage[]): ChatMessage[] { + // Create a copy of the messages array + const newMessages = [...messages]; + + // Find the last user message + const lastUserMessageIndex = newMessages.findLastIndex((msg) => msg.role === "user"); + + if (lastUserMessageIndex === -1) { + // No user message found + if (newMessages.length > 0) { + // Insert prompts before the last message, which is expected to be empty assistant + newMessages.splice(newMessages.length - 1, 0, ...prompt); + } else { + // Return a single message array with just the prompt + return prompt; + } + } else { + + const lastUserPromptIndex = prompt.findLastIndex((msg) => msg.role === "user"); + + let promptContent: MessagePart[]; + if (lastUserPromptIndex === -1) { + // No user messages in prompt, insert them before the last user message + newMessages.splice(lastUserMessageIndex, 0, ...prompt); + promptContent = []; + } else { + // User messages in prompt, insert all but last user prompt before the last user message + newMessages.splice(lastUserMessageIndex, 0, ...prompt.slice(0, lastUserPromptIndex )); + promptContent = prompt[lastUserPromptIndex].content as MessagePart[]; + } + // Get the last user message + const userMessage = newMessages[lastUserMessageIndex]; + + // Convert string content to MessagePart[] if needed + let contentAsArray: MessagePart[]; + const content = userMessage.content; + if (typeof content === "string") { + contentAsArray = [{ type: "text", text: content }]; + } else { + contentAsArray = userMessage.content as MessagePart[]; + } + + // Apply substituteContent function + const newContent = substituteContent(contentAsArray, input, promptContent); + + // Create a new user message with the substituted content + const newUserMessage: UserChatMessage = { + role: "user", + content: newContent + }; + + // Replace the old user message with the new one + newMessages[lastUserMessageIndex] = newUserMessage; + + } + return newMessages; +} + export function constructMcpSlashCommand( client: Client, name: string, @@ -11,36 +124,63 @@ export function constructMcpSlashCommand( return { name, description: description ?? "MCP Prompt", - params: {}, - run: async function* (context) { + // params: {}, + run: async function* ({ input, llm, ide, history, completionOptions }) { + // Extract user input after the command + const userInput = input.startsWith(`/${name}`) + ? input.slice(name.length + 1).trimStart() + : input; + + // Prepare arguments for MCP client + // some special arguments to tell MCP about the context + const workspaceDirs = JSON.stringify(await ide.getWorkspaceDirs()); const argsObject: { [key: string]: string } = {}; - const userInput = context.input.split(" ").slice(1).join(" "); if (args) { - args.forEach((arg, i) => { - argsObject[arg] = userInput; + args.forEach((arg) => { + switch (arg) { + case "model":argsObject["model"] = llm.model; + break; + case "workspaceDirs": argsObject["workspaceDirs"] = workspaceDirs; + break; + case "history": argsObject["history"] = JSON.stringify(history); + break; + default: + argsObject[arg] = userInput; + } }); } + argsObject["workspaceDirs"] = (await ide.getWorkspaceDirs()).join(","); + argsObject["model"] = llm.model; + // Get prompt from MCP const result = await client.getPrompt({ name, arguments: argsObject }); - const messages: ChatMessage[] = result.messages.map((msg) => { - if (msg.content.type !== "text") { - throw new Error( - "Continue currently only supports text prompts through MCP", - ); + // Convert MCP messages to ChatMessage format + const mcpMessages: ChatMessage[] = result.messages.map((msg) => { + if (msg.content.type === "text") { + return { + content: [{ + type: "text", + text: msg.content.text, + }], + role: msg.role, + }; + } else { + throw new Error(`Unsupported message type: ${msg.content.type}`); } - return { - content: msg.content.text, - role: msg.role, - }; }); - for await (const chunk of context.llm.streamChat( + // substitute prompt into history + const messages = substituteLastUserMessage(history, `/${name} `, mcpMessages); + + // Stream the response + for await (const chunk of llm.streamChat( messages, new AbortController().signal, + completionOptions, )) { yield renderChatMessage(chunk); } }, }; -} +} \ No newline at end of file diff --git a/core/context/providers/MCPContextProvider.ts b/core/context/providers/MCPContextProvider.ts index 0cb8e2c668..77671731f5 100644 --- a/core/context/providers/MCPContextProvider.ts +++ b/core/context/providers/MCPContextProvider.ts @@ -46,8 +46,14 @@ class MCPContextProvider extends BaseContextProvider { if (!connection) { throw new Error(`No MCP connection found for ${mcpId}`); } + // some special arguments to tell MCP about the context + // for example, to extract the workspace directories or project structure, + // or to adapt context to the current model + // MCP protocol allows any additional arguments, server will ignore unknown + const workspaceDirs = JSON.stringify(await extras.ide.getWorkspaceDirs()); + const model = extras.llm.model; - const { contents } = await connection.client.readResource({ uri }); + const { contents } = await connection.client.readResource({ uri, model, workspaceDirs }); return await Promise.all( contents.map(async (resource) => { diff --git a/core/tools/callTool.ts b/core/tools/callTool.ts index 959752af18..f1c636d07e 100644 --- a/core/tools/callTool.ts +++ b/core/tools/callTool.ts @@ -77,9 +77,17 @@ async function callToolFromUri( if (!client) { throw new Error("MCP connection not found"); } + // some special arguments to tell MCP about the context + // for example, to extract the workspace directories or project structure, + // or to adapt context to the current model + // MCP protocol allows any additional arguments, server will ignore unknown + const workspaceDirs = JSON.stringify(await extras.ide.getWorkspaceDirs()); + const model = extras.llm.model; const response = await client.client.callTool({ name: toolName, arguments: args, + model, + workspaceDirs, }); if (response.isError === true) { From 8649fc21edebedff38dac243a75fff583545e6bf Mon Sep 17 00:00:00 2001 From: Alexander Smirnov Date: Sat, 1 Mar 2025 20:20:48 -0800 Subject: [PATCH 2/3] implement listRoots handler, return list of ide work dirs --- core/commands/slash/mcp.ts | 24 ++++++++---- core/config/load.ts | 2 +- core/config/yaml/loadYaml.ts | 2 +- core/context/mcp/index.ts | 39 ++++++++++++++++++-- core/context/providers/MCPContextProvider.ts | 9 +---- core/tools/callTool.ts | 8 ---- 6 files changed, 54 insertions(+), 30 deletions(-) diff --git a/core/commands/slash/mcp.ts b/core/commands/slash/mcp.ts index 7908f20e80..ac2df4fba9 100644 --- a/core/commands/slash/mcp.ts +++ b/core/commands/slash/mcp.ts @@ -3,6 +3,19 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { ChatMessage, MessagePart, SlashCommand, UserChatMessage } from "../../index.js"; import { renderChatMessage } from "../../util/messageContent.js"; +function findLastIndex(array: T[], predicate: (value: T) => boolean): number { + if (!array || array.length === 0) { + return -1; + } + + for (let i = array.length - 1; i >= 0; i--) { + if (predicate(array[i])) { + return i; + } + } + + return -1; +} /** * Function substitutes the content of the message with the prompt * Find the last text message that contains the input in content, @@ -21,7 +34,7 @@ function substituteContent(content: MessagePart[], input: string, prompt: Messag const newContent = [...content]; // Find the last text message part that contains the input - const foundIndex = newContent.findLastIndex((part) => part.type === "text" && part.text.includes(input)); + const foundIndex = findLastIndex(newContent,(part) => part.type === "text" && part.text.includes(input)); if (foundIndex !== -1) { // Found a part containing the input @@ -62,7 +75,7 @@ function substituteLastUserMessage(messages: ChatMessage[], input: string, promp const newMessages = [...messages]; // Find the last user message - const lastUserMessageIndex = newMessages.findLastIndex((msg) => msg.role === "user"); + const lastUserMessageIndex = findLastIndex(newMessages,(msg) => msg.role === "user"); if (lastUserMessageIndex === -1) { // No user message found @@ -75,7 +88,7 @@ function substituteLastUserMessage(messages: ChatMessage[], input: string, promp } } else { - const lastUserPromptIndex = prompt.findLastIndex((msg) => msg.role === "user"); + const lastUserPromptIndex = findLastIndex(prompt,(msg) => msg.role === "user"); let promptContent: MessagePart[]; if (lastUserPromptIndex === -1) { @@ -133,15 +146,12 @@ export function constructMcpSlashCommand( // Prepare arguments for MCP client // some special arguments to tell MCP about the context - const workspaceDirs = JSON.stringify(await ide.getWorkspaceDirs()); const argsObject: { [key: string]: string } = {}; if (args) { args.forEach((arg) => { switch (arg) { case "model":argsObject["model"] = llm.model; break; - case "workspaceDirs": argsObject["workspaceDirs"] = workspaceDirs; - break; case "history": argsObject["history"] = JSON.stringify(history); break; default: @@ -149,8 +159,6 @@ export function constructMcpSlashCommand( } }); } - argsObject["workspaceDirs"] = (await ide.getWorkspaceDirs()).join(","); - argsObject["model"] = llm.model; // Get prompt from MCP const result = await client.getPrompt({ name, arguments: argsObject }); diff --git a/core/config/load.ts b/core/config/load.ts index 5abb3fd5eb..54fa698dcd 100644 --- a/core/config/load.ts +++ b/core/config/load.ts @@ -550,7 +550,7 @@ async function intermediateToFinalConfig( async (server, index) => { try { const mcpId = index.toString(); - const mcpConnection = mcpManager.createConnection(mcpId, server); + const mcpConnection = mcpManager.createConnection(mcpId, server, ide); if (!mcpConnection) { return; } diff --git a/core/config/yaml/loadYaml.ts b/core/config/yaml/loadYaml.ts index b0cabac3ba..349ed70b5f 100644 --- a/core/config/yaml/loadYaml.ts +++ b/core/config/yaml/loadYaml.ts @@ -347,7 +347,7 @@ async function configYamlToContinueConfig( args: [], ...server, }, - }); + },ide); if (!mcpConnection) { return; } diff --git a/core/context/mcp/index.ts b/core/context/mcp/index.ts index 19de5434a3..4f173d91b1 100644 --- a/core/context/mcp/index.ts +++ b/core/context/mcp/index.ts @@ -5,11 +5,30 @@ import { WebSocketClientTransport } from "@modelcontextprotocol/sdk/client/webso import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { ConfigValidationError } from "@continuedev/config-yaml"; -import { ContinueConfig, MCPOptions, SlashCommand, Tool } from "../.."; +import { ContinueConfig, IDE, MCPOptions, SlashCommand, Tool } from "../.."; import { constructMcpSlashCommand } from "../../commands/slash/mcp"; import { encodeMCPToolUri } from "../../tools/callTool"; import MCPContextProvider from "../providers/MCPContextProvider"; - +import { ListRootsRequestSchema, RootSchema } from "@modelcontextprotocol/sdk/types.js"; + +/** + * Converts a file path root response. + * @param filePath The full file path to convert + * @returns root object with name as file, uri as properly formatted file URL + */ +function filePathToRoot(filePath: string): { name: string; uri: string } { + const normalizedPath = filePath.replace(/\\/g, '/'); + + const pathPrefix = normalizedPath.startsWith('/') ? 'file://' : 'file:///'; + const name = normalizedPath.split('/').pop() || ''; + try { + const url = new URL(normalizedPath, pathPrefix); + return { name, uri: url.href }; + } catch (error) { + // For simple cases, fall back to manual construction + return { name, uri: `${pathPrefix}${encodeURI(normalizedPath)}`}; + } +} export class MCPManagerSingleton { private static instance: MCPManagerSingleton; @@ -24,10 +43,18 @@ export class MCPManagerSingleton { return MCPManagerSingleton.instance; } - createConnection(id: string, options: MCPOptions): MCPConnection | undefined { + createConnection(id: string, options: MCPOptions, ide: IDE): MCPConnection | undefined { if (!this.connections.has(id)) { const connection = new MCPConnection(options); this.connections.set(id, connection); + // handle listRoots request + connection.client.setRequestHandler(ListRootsRequestSchema, async (request,extra ) => { + const roots = (await ide.getWorkspaceDirs()).map((root) => filePathToRoot(root)); + return { + roots + }; + }); + return connection; } else { return this.connections.get(id); @@ -61,7 +88,11 @@ class MCPConnection { version: "1.0.0", }, { - capabilities: {}, + capabilities: { + roots: { + listChanged: true, + }, + }, }, ); } diff --git a/core/context/providers/MCPContextProvider.ts b/core/context/providers/MCPContextProvider.ts index 77671731f5..5cee86f63b 100644 --- a/core/context/providers/MCPContextProvider.ts +++ b/core/context/providers/MCPContextProvider.ts @@ -46,14 +46,7 @@ class MCPContextProvider extends BaseContextProvider { if (!connection) { throw new Error(`No MCP connection found for ${mcpId}`); } - // some special arguments to tell MCP about the context - // for example, to extract the workspace directories or project structure, - // or to adapt context to the current model - // MCP protocol allows any additional arguments, server will ignore unknown - const workspaceDirs = JSON.stringify(await extras.ide.getWorkspaceDirs()); - const model = extras.llm.model; - - const { contents } = await connection.client.readResource({ uri, model, workspaceDirs }); + const { contents } = await connection.client.readResource({ uri }); return await Promise.all( contents.map(async (resource) => { diff --git a/core/tools/callTool.ts b/core/tools/callTool.ts index f1c636d07e..959752af18 100644 --- a/core/tools/callTool.ts +++ b/core/tools/callTool.ts @@ -77,17 +77,9 @@ async function callToolFromUri( if (!client) { throw new Error("MCP connection not found"); } - // some special arguments to tell MCP about the context - // for example, to extract the workspace directories or project structure, - // or to adapt context to the current model - // MCP protocol allows any additional arguments, server will ignore unknown - const workspaceDirs = JSON.stringify(await extras.ide.getWorkspaceDirs()); - const model = extras.llm.model; const response = await client.client.callTool({ name: toolName, arguments: args, - model, - workspaceDirs, }); if (response.isError === true) { From 868d1dcd9bb26636000a134a7d85e395b0b1892c Mon Sep 17 00:00:00 2001 From: Alexander Smirnov Date: Tue, 4 Mar 2025 16:25:08 -0800 Subject: [PATCH 3/3] add test for MCP slash command --- core/commands/slash/mcp.test.ts | 311 ++++++++++++++++++++++++++++++++ core/commands/slash/mcp.ts | 9 +- 2 files changed, 316 insertions(+), 4 deletions(-) create mode 100644 core/commands/slash/mcp.test.ts diff --git a/core/commands/slash/mcp.test.ts b/core/commands/slash/mcp.test.ts new file mode 100644 index 0000000000..7f96c0b94b --- /dev/null +++ b/core/commands/slash/mcp.test.ts @@ -0,0 +1,311 @@ +import { substituteLastUserMessage } from "./mcp"; +import { ChatMessage, MessagePart, SlashCommand, UserChatMessage } from "../../index.js"; + +/** + * Test substitution of the prompt content into the history. + * actual content collected from debugging actual chats. + */ +describe("substituteLastUserMessage", () => { + const systemMessage: ChatMessage = { + role: "system", + content: "Your are..." + }; + + const assistantEmptyMessage: ChatMessage = { + role: "assistant", + content: "" + }; + + const mcpMessage: ChatMessage = { + role: "user", + content: [{ type: "text", text: "provide answer" }] + }; + const mcpHeadMessage: ChatMessage = { + role: "user", + content: [{ type: "text", text: "this is important task" }] + }; + const mcpAssistantMessage: ChatMessage = { + role: "assistant", + content: [{ type: "text", text: "I'm assistant" }] + }; + + it("should handle only /echo on first question", () => { + // Input: `/echo ` + const history: ChatMessage[] = [ + systemMessage, + { + role: "user", + content: [{ type: "text", text: "/echo " }] + }, + assistantEmptyMessage + ]; + + const expected: ChatMessage[] = [ + systemMessage, + { + role: "user", + content: [{ type: "text", text: "provide answer" }, { type: "text", text: "" }] + }, + assistantEmptyMessage + ]; + + const result = substituteLastUserMessage(history, "/echo ", [mcpMessage]); + expect(result).toEqual(expected); + }); + + it("should handle /echo with text on first question", () => { + // Input: `/echo hello` + const history: ChatMessage[] = [ + systemMessage, + { + role: "user", + content: [{ type: "text", text: "/echo hello" }] + }, + assistantEmptyMessage + ]; + + const expected: ChatMessage[] = [ + systemMessage, + { + role: "user", + content: [{ type: "text", text: "provide answer" }, { type: "text", text: "hello" }] + }, + assistantEmptyMessage + ]; + + const result = substituteLastUserMessage(history, "/echo ", [mcpMessage]); + expect(result).toEqual(expected); + }); + + it("should handle /echo with text and 1 context on first question", () => { + // Input: `/echo explain text.ts code` + const history: ChatMessage[] = [ + systemMessage, + { + role: "user", + content: [ + { type: "text", text: "text.ts code block" }, + { type: "text", text: "/echo explain text.ts code" } + ] + }, + assistantEmptyMessage + ]; + + const expected: ChatMessage[] = [ + systemMessage, + { + role: "user", + content: [ + { type: "text", text: "text.ts code block" }, + { type: "text", text: "provide answer" }, + { type: "text", text: "explain text.ts code" } + ] + }, + assistantEmptyMessage + ]; + + const result = substituteLastUserMessage(history, "/echo ", [mcpMessage]); + expect(result).toEqual(expected); + }); + + it("should handle /echo with text and 2 contexts on first question", () => { + // Input: `/echo explain test.ts and test.py` + const history: ChatMessage[] = [ + systemMessage, + { + role: "user", + content: [ + { type: "text", text: "text.ts code block" }, + { type: "text", text: "text.py code block" }, + { type: "text", text: "/echo explain test.ts and test.py " } + ] + }, + assistantEmptyMessage + ]; + + const expected: ChatMessage[] = [ + systemMessage, + { + role: "user", + content: [ + { type: "text", text: "text.ts code block" }, + { type: "text", text: "text.py code block" }, + { type: "text", text: "provide answer" }, + { type: "text", text: "explain test.ts and test.py " } + ] + }, + assistantEmptyMessage + ]; + + const result = substituteLastUserMessage(history, "/echo ", [mcpMessage]); + expect(result).toEqual(expected); + }); + + it("should handle /echo with text and image on first question", () => { + // Input: `/echo explain` + const history: ChatMessage[] = [ + systemMessage, + { + role: "user", + content: [ + { type: "imageUrl", imageUrl:{ url: "data:..."} }, + { type: "text", text: "/echo explain" } + ] + }, + assistantEmptyMessage + ]; + + const expected: ChatMessage[] = [ + systemMessage, + { + role: "user", + content: [ + { type: "imageUrl", imageUrl:{ url: "data:..."} }, + { type: "text", text: "provide answer" }, + { type: "text", text: "explain" } + ] + }, + assistantEmptyMessage + ]; + + const result = substituteLastUserMessage(history, "/echo ", [mcpMessage]); + expect(result).toEqual(expected); + }); + + it("should handle /echo with text, image, and context on first question", () => { + // Input: `/echo explain test.ts` + const history: ChatMessage[] = [ + systemMessage, + { + role: "user", + content: [ + { type: "text", text: "text.ts code block" }, + { type: "imageUrl", imageUrl:{ url: "data:..."} }, + { type: "text", text: "/echo explain test.ts" } + ] + }, + assistantEmptyMessage + ]; + + const expected: ChatMessage[] = [ + systemMessage, + { + role: "user", + content: [ + { type: "text", text: "text.ts code block" }, + { type: "imageUrl", imageUrl:{ url: "data:..."} }, + { type: "text", text: "provide answer" }, + { type: "text", text: "explain test.ts" } + ] + }, + assistantEmptyMessage + ]; + + const result = substituteLastUserMessage(history, "/echo ", [mcpMessage]); + expect(result).toEqual(expected); + }); + + it("should handle only /echo on second question", () => { + // Input: `/echo ` + const history: ChatMessage[] = [ + systemMessage, + { role: "user", content: "first history" }, + { role: "assistant", content: "answer" }, + { + role: "user", + content: [{ type: "text", text: "/echo " }] + }, + assistantEmptyMessage + ]; + + const expected: ChatMessage[] = [ + systemMessage, + { role: "user", content: "first history" }, + { role: "assistant", content: "answer" }, + { + role: "user", + content: [{ type: "text", text: "provide answer" }, { type: "text", text: "" }] + }, + assistantEmptyMessage + ]; + + const result = substituteLastUserMessage(history, "/echo ", [mcpMessage]); + expect(result).toEqual(expected); + }); + + it("should handle /echo with text and resource on second question", () => { + // Same as first question but with more history at the start + const history: ChatMessage[] = [ + systemMessage, + { role: "user", content: "previous question" }, + { role: "assistant", content: "previous answer" }, + { + role: "user", + content: [ + { type: "text", text: "text.ts code block" }, + { type: "imageUrl", imageUrl:{ url: "data:..."} }, + { type: "text", text: "/echo explain test.ts" } + ] + }, + assistantEmptyMessage + ]; + + const expected: ChatMessage[] = [ + systemMessage, + { role: "user", content: "previous question" }, + { role: "assistant", content: "previous answer" }, + { + role: "user", + content: [ + { type: "text", text: "text.ts code block" }, + { type: "imageUrl", imageUrl:{ url: "data:..."}}, + { type: "text", text: "provide answer" }, + { type: "text", text: "explain test.ts" } + ] + }, + assistantEmptyMessage + ]; + + const result = substituteLastUserMessage(history, "/echo ", [mcpMessage]); + expect(result).toEqual(expected); + }); + + it("should handle /echo with text and resource with multisection prompt", () => { + // Same as first question but with more history at the start + const history: ChatMessage[] = [ + systemMessage, + { role: "user", content: "previous question" }, + { role: "assistant", content: "previous answer" }, + { + role: "user", + content: [ + { type: "text", text: "text.ts code block" }, + { type: "imageUrl", imageUrl:{ url: "data:..."} }, + { type: "text", text: "/echo explain test.ts" } + ] + }, + assistantEmptyMessage + ]; + + const expected: ChatMessage[] = [ + systemMessage, + { role: "user", content: "previous question" }, + { role: "assistant", content: "previous answer" }, + mcpHeadMessage, + mcpAssistantMessage, + { + role: "user", + content: [ + { type: "text", text: "text.ts code block" }, + { type: "imageUrl", imageUrl:{ url: "data:..."}}, + { type: "text", text: "provide answer" }, + { type: "text", text: "explain test.ts" } + ] + }, + assistantEmptyMessage + ]; + + const result = substituteLastUserMessage(history, "/echo ", [mcpHeadMessage,mcpAssistantMessage,mcpMessage]); + expect(result).toEqual(expected); + }); +}); \ No newline at end of file diff --git a/core/commands/slash/mcp.ts b/core/commands/slash/mcp.ts index ac2df4fba9..9194514a07 100644 --- a/core/commands/slash/mcp.ts +++ b/core/commands/slash/mcp.ts @@ -70,12 +70,12 @@ function substituteContent(content: MessagePart[], input: string, prompt: Messag * @param prompt Replacement content to be inserted * @returns New array of messages with the substitution applied */ -function substituteLastUserMessage(messages: ChatMessage[], input: string, prompt: ChatMessage[]): ChatMessage[] { +export function substituteLastUserMessage(messages: ChatMessage[], input: string, prompt: ChatMessage[]): ChatMessage[] { // Create a copy of the messages array const newMessages = [...messages]; // Find the last user message - const lastUserMessageIndex = findLastIndex(newMessages,(msg) => msg.role === "user"); + let lastUserMessageIndex = findLastIndex(newMessages,(msg) => msg.role === "user"); if (lastUserMessageIndex === -1) { // No user message found @@ -95,10 +95,11 @@ function substituteLastUserMessage(messages: ChatMessage[], input: string, promp // No user messages in prompt, insert them before the last user message newMessages.splice(lastUserMessageIndex, 0, ...prompt); promptContent = []; - } else { + } else { // User messages in prompt, insert all but last user prompt before the last user message newMessages.splice(lastUserMessageIndex, 0, ...prompt.slice(0, lastUserPromptIndex )); promptContent = prompt[lastUserPromptIndex].content as MessagePart[]; + lastUserMessageIndex += lastUserPromptIndex } // Get the last user message const userMessage = newMessages[lastUserMessageIndex]; @@ -112,7 +113,7 @@ function substituteLastUserMessage(messages: ChatMessage[], input: string, promp contentAsArray = userMessage.content as MessagePart[]; } - // Apply substituteContent function + // replace command with prompt in content const newContent = substituteContent(contentAsArray, input, promptContent); // Create a new user message with the substituted content