mirror of
https://github.com/voideditor/void
synced 2026-05-23 09:28:23 +00:00
Merge pull request #630 from voideditor/mcp
bjoaquinc/mcp - MCP Implementation
This commit is contained in:
commit
c12e9ca9f8
30 changed files with 1942 additions and 377 deletions
|
|
@ -8,3 +8,5 @@ Look for services and built-in functions that you might need to use to solve the
|
|||
In typescript, do NOT cast to types if not neccessary. NEVER lazily cast to 'any'. Find the correct type to apply and use it.
|
||||
|
||||
Do not add or remove semicolons to any of my files. Just go with convention and make the least number of changes.
|
||||
|
||||
Never modify files outside src/vs/workbench/contrib/void without consulting with the user first.
|
||||
|
|
|
|||
|
|
@ -842,7 +842,9 @@ export default tseslint.config(
|
|||
'@xterm/xterm',
|
||||
'yauzl',
|
||||
'yazl',
|
||||
'zlib'
|
||||
'zlib',
|
||||
// Void added this
|
||||
'@modelcontextprotocol/sdk/**'
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -17,7 +17,7 @@
|
|||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
"@microsoft/1ds-post-js": "^3.2.13",
|
||||
"@mistralai/mistralai": "^1.6.0",
|
||||
"@modelcontextprotocol/sdk": "^1.10.2",
|
||||
"@modelcontextprotocol/sdk": "^1.11.2",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@vscode/deviceid": "^0.1.1",
|
||||
|
|
@ -2616,9 +2616,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.10.2.tgz",
|
||||
"integrity": "sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==",
|
||||
"version": "1.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.2.tgz",
|
||||
"integrity": "sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^1.0.5",
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
"@microsoft/1ds-post-js": "^3.2.13",
|
||||
"@mistralai/mistralai": "^1.6.0",
|
||||
"@modelcontextprotocol/sdk": "^1.10.2",
|
||||
"@modelcontextprotocol/sdk": "^1.11.2",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@vscode/deviceid": "^0.1.1",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ import { IVoidUpdateService } from '../../workbench/contrib/void/common/voidUpda
|
|||
import { MetricsMainService } from '../../workbench/contrib/void/electron-main/metricsMainService.js';
|
||||
import { VoidMainUpdateService } from '../../workbench/contrib/void/electron-main/voidUpdateMainService.js';
|
||||
import { LLMMessageChannel } from '../../workbench/contrib/void/electron-main/sendLLMMessageChannel.js';
|
||||
import { MCPChannel } from '../../workbench/contrib/void/electron-main/mcpChannel.js';
|
||||
|
||||
/**
|
||||
* The main VS Code application. There will only ever be one instance,
|
||||
|
|
@ -1243,6 +1244,9 @@ export class CodeApplication extends Disposable {
|
|||
const sendLLMMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
|
||||
mainProcessElectronServer.registerChannel('void-channel-llmMessage', sendLLMMessageChannel);
|
||||
|
||||
const mcpChannel = new MCPChannel();
|
||||
mainProcessElectronServer.registerChannel('void-channel-mcp', mcpChannel);
|
||||
|
||||
// Extension Host Debug Broadcasting
|
||||
const electronExtensionHostDebugBroadcastChannel = new ElectronExtensionHostDebugBroadcastChannel(accessor.get(IWindowsMainService));
|
||||
mainProcessElectronServer.registerChannel('extensionhostdebugservice', electronExtensionHostDebugBroadcastChannel);
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
|
|||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
|
||||
import { chat_userMessageContent, ToolName, } from '../common/prompt/prompts.js';
|
||||
import { chat_userMessageContent, isABuiltinToolName } from '../common/prompt/prompts.js';
|
||||
import { AnthropicReasoning, getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js';
|
||||
import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, ToolCallParams, ToolName, ToolResult } from '../common/toolsServiceTypes.js';
|
||||
import { IToolsService } from './toolsService.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
|
||||
|
|
@ -37,6 +37,8 @@ import { deepClone } from '../../../../base/common/objects.js';
|
|||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IDirectoryStrService } from '../common/directoryStrService.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IMCPService } from '../common/mcpService.js';
|
||||
import { RawMCPToolCall } from '../common/mcpServiceTypes.js';
|
||||
|
||||
|
||||
// related to retrying when LLM message has error
|
||||
|
|
@ -181,10 +183,11 @@ export type ThreadStreamState = {
|
|||
llmInfo?: undefined;
|
||||
toolInfo: {
|
||||
toolName: ToolName;
|
||||
toolParams: ToolCallParams[ToolName];
|
||||
toolParams: ToolCallParams<ToolName>;
|
||||
id: string;
|
||||
content: string;
|
||||
rawParams: RawToolParamsObj;
|
||||
mcpServerName: string | undefined;
|
||||
};
|
||||
interrupt: Promise<() => void>;
|
||||
} | {
|
||||
|
|
@ -323,6 +326,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
||||
@IDirectoryStrService private readonly _directoryStringService: IDirectoryStrService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@IMCPService private readonly _mcpService: IMCPService,
|
||||
) {
|
||||
super()
|
||||
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
|
||||
|
|
@ -445,7 +449,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
// if running now but stream state doesn't indicate it (happens if restart Void), cancel that last tool
|
||||
if (lastMessage && lastMessage.role === 'tool' && lastMessage.type === 'running_now') {
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', content: lastMessage.content, id: lastMessage.id, rawParams: lastMessage.rawParams, result: null, name: lastMessage.name, params: lastMessage.params })
|
||||
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', content: lastMessage.content, id: lastMessage.id, rawParams: lastMessage.rawParams, result: null, name: lastMessage.name, params: lastMessage.params, mcpServerName: lastMessage.mcpServerName })
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -532,19 +537,23 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
const lastMsg = thread.messages[thread.messages.length - 1]
|
||||
|
||||
let params: ToolCallParams[ToolName]
|
||||
let params: ToolCallParams<ToolName>
|
||||
if (lastMsg.role === 'tool' && lastMsg.type !== 'invalid_params') {
|
||||
params = lastMsg.params
|
||||
}
|
||||
else return
|
||||
|
||||
const { name, id, rawParams } = lastMsg
|
||||
const { name, id, rawParams, mcpServerName } = lastMsg
|
||||
|
||||
const errorMessage = this.toolErrMsgs.rejected
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams })
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams, mcpServerName })
|
||||
this._setStreamState(threadId, undefined)
|
||||
}
|
||||
|
||||
private _computeMCPServerOfToolName = (toolName: string) => {
|
||||
return this._mcpService.getMCPTools()?.find(t => t.name === toolName)?.mcpServerName
|
||||
}
|
||||
|
||||
async abortRunning(threadId: string) {
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return // should never happen
|
||||
|
|
@ -553,13 +562,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
if (this.streamState[threadId]?.isRunning === 'LLM') {
|
||||
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
||||
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
|
||||
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name, mcpServerName: this._computeMCPServerOfToolName(toolCallSoFar.name) })
|
||||
}
|
||||
// add tool that's running
|
||||
else if (this.streamState[threadId]?.isRunning === 'tool') {
|
||||
const { toolName, toolParams, id, content: content_, rawParams } = this.streamState[threadId].toolInfo
|
||||
const { toolName, toolParams, id, content: content_, rawParams, mcpServerName } = this.streamState[threadId].toolInfo
|
||||
const content = content_ || this.toolErrMsgs.interrupted
|
||||
this._updateLatestTool(threadId, { role: 'tool', name: toolName, params: toolParams, id, content, rawParams, type: 'rejected', result: null })
|
||||
this._updateLatestTool(threadId, { role: 'tool', name: toolName, params: toolParams, id, content, rawParams, type: 'rejected', result: null, mcpServerName })
|
||||
}
|
||||
// reject the tool for the user if relevant
|
||||
else if (this.streamState[threadId]?.isRunning === 'awaiting_user') {
|
||||
|
|
@ -597,36 +606,46 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
threadId: string,
|
||||
toolName: ToolName,
|
||||
toolId: string,
|
||||
opts: { preapproved: true, unvalidatedToolParams: RawToolParamsObj, validatedParams: ToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj },
|
||||
mcpServerName: string | undefined,
|
||||
opts: { preapproved: true, unvalidatedToolParams: RawToolParamsObj, validatedParams: ToolCallParams<ToolName> } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj },
|
||||
): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => {
|
||||
|
||||
// compute these below
|
||||
let toolParams: ToolCallParams[ToolName]
|
||||
let toolResult: Awaited<ToolResultType[typeof toolName]>
|
||||
let toolParams: ToolCallParams<ToolName>
|
||||
let toolResult: ToolResult<ToolName>
|
||||
let toolResultStr: string
|
||||
|
||||
// Check if it's a built-in tool
|
||||
const isBuiltInTool = isABuiltinToolName(toolName)
|
||||
|
||||
|
||||
if (!opts.preapproved) { // skip this if pre-approved
|
||||
// 1. validate tool params
|
||||
try {
|
||||
const params = this._toolsService.validateParams[toolName](opts.unvalidatedToolParams)
|
||||
toolParams = params
|
||||
} catch (error) {
|
||||
if (isBuiltInTool) {
|
||||
const params = this._toolsService.validateParams[toolName](opts.unvalidatedToolParams)
|
||||
toolParams = params
|
||||
}
|
||||
else {
|
||||
toolParams = opts.unvalidatedToolParams
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', rawParams: opts.unvalidatedToolParams, result: null, name: toolName, content: errorMessage, id: toolId, })
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', rawParams: opts.unvalidatedToolParams, result: null, name: toolName, content: errorMessage, id: toolId, mcpServerName })
|
||||
return {}
|
||||
}
|
||||
// once validated, add checkpoint for edit
|
||||
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) }
|
||||
if (toolName === 'rewrite_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['rewrite_file']).uri }) }
|
||||
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as BuiltinToolCallParams['edit_file']).uri }) }
|
||||
if (toolName === 'rewrite_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as BuiltinToolCallParams['rewrite_file']).uri }) }
|
||||
|
||||
// 2. if tool requires approval, break from the loop, awaiting approval
|
||||
|
||||
|
||||
const approvalType = approvalTypeOfToolName[toolName]
|
||||
const approvalType = isBuiltInTool ? approvalTypeOfBuiltinToolName[toolName] : 'mcp-tools'
|
||||
if (approvalType) {
|
||||
const autoApprove = this._settingsService.state.globalSettings.autoApprove[approvalType]
|
||||
// add a tool_request because we use it for UI if a tool is loading (this should be improved in the future)
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(Awaiting user permission...)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(Awaiting user permission...)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName })
|
||||
if (!autoApprove) {
|
||||
return { awaitingUserApproval: true }
|
||||
}
|
||||
|
|
@ -638,9 +657,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 3. call the tool
|
||||
// this._setStreamState(threadId, { isRunning: 'tool' }, 'merge')
|
||||
const runningTool = { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null, id: toolId, rawParams: opts.unvalidatedToolParams } as const
|
||||
const runningTool = { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName } as const
|
||||
this._updateLatestTool(threadId, runningTool)
|
||||
|
||||
|
||||
|
|
@ -650,13 +672,28 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
try {
|
||||
|
||||
// set stream state
|
||||
this._setStreamState(threadId, { isRunning: 'tool', interrupt: interruptorPromise, toolInfo: { toolName, toolParams, id: toolId, content: 'interrupted...', rawParams: opts.unvalidatedToolParams } })
|
||||
this._setStreamState(threadId, { isRunning: 'tool', interrupt: interruptorPromise, toolInfo: { toolName, toolParams, id: toolId, content: 'interrupted...', rawParams: opts.unvalidatedToolParams, mcpServerName } })
|
||||
|
||||
const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any)
|
||||
const interruptor = () => { interrupted = true; interruptTool?.() }
|
||||
resolveInterruptor(interruptor)
|
||||
if (isBuiltInTool) {
|
||||
const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any)
|
||||
const interruptor = () => { interrupted = true; interruptTool?.() }
|
||||
resolveInterruptor(interruptor)
|
||||
|
||||
toolResult = await result
|
||||
toolResult = await result
|
||||
}
|
||||
else {
|
||||
const mcpTools = this._mcpService.getMCPTools()
|
||||
const mcpTool = mcpTools?.find(t => t.name === toolName)
|
||||
if (!mcpTool) { throw new Error(`MCP tool ${toolName} not found`) }
|
||||
|
||||
resolveInterruptor(() => { })
|
||||
|
||||
toolResult = (await this._mcpService.callMCPTool({
|
||||
serverName: mcpTool.mcpServerName ?? 'unknown_mcp_server',
|
||||
toolName: toolName,
|
||||
params: toolParams
|
||||
})).result
|
||||
}
|
||||
|
||||
if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here
|
||||
}
|
||||
|
|
@ -665,21 +702,27 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here
|
||||
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName })
|
||||
return {}
|
||||
}
|
||||
|
||||
// 4. stringify the result to give to the LLM
|
||||
try {
|
||||
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
|
||||
if (isBuiltInTool) {
|
||||
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
|
||||
}
|
||||
// For MCP tools, handle the result based on its type
|
||||
else {
|
||||
toolResultStr = this._mcpService.stringifyResult(toolResult as RawMCPToolCall)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = this.toolErrMsgs.errWhenStringifying(error)
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName })
|
||||
return {}
|
||||
}
|
||||
|
||||
// 5. add to history and keep going
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, id: toolId, rawParams: opts.unvalidatedToolParams })
|
||||
this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName })
|
||||
return {}
|
||||
};
|
||||
|
||||
|
|
@ -714,7 +757,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
// before enter loop, call tool
|
||||
if (callThisToolFirst) {
|
||||
const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params })
|
||||
const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, callThisToolFirst.mcpServerName, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params })
|
||||
if (interrupted) {
|
||||
this._setStreamState(threadId, undefined)
|
||||
this._addUserCheckpoint({ threadId })
|
||||
|
|
@ -823,7 +866,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
const { error } = llmRes
|
||||
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
||||
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
|
||||
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name, mcpServerName: this._computeMCPServerOfToolName(toolCallSoFar.name) })
|
||||
|
||||
this._setStreamState(threadId, { isRunning: undefined, error })
|
||||
this._addUserCheckpoint({ threadId })
|
||||
|
|
@ -840,7 +883,10 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
// call tool if there is one
|
||||
if (toolCall) {
|
||||
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, toolCall.name, toolCall.id, { preapproved: false, unvalidatedToolParams: toolCall.rawParams })
|
||||
const mcpTools = this._mcpService.getMCPTools()
|
||||
const mcpTool = mcpTools?.find(t => t.name === toolCall.name)
|
||||
|
||||
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, toolCall.name, toolCall.id, mcpTool?.mcpServerName, { preapproved: false, unvalidatedToolParams: toolCall.rawParams })
|
||||
if (interrupted) {
|
||||
this._setStreamState(threadId, undefined)
|
||||
return
|
||||
|
|
@ -1300,7 +1346,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
}
|
||||
// URIs of files that have been read
|
||||
else if (m.role === 'tool' && m.type === 'success' && m.name === 'read_file') {
|
||||
const params = m.params as ToolCallParams['read_file']
|
||||
const params = m.params as BuiltinToolCallParams['read_file']
|
||||
addURI(params.uri)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/
|
|||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { ChatMessage } from '../common/chatThreadServiceTypes.js';
|
||||
import { getIsReasoningEnabledState, getReservedOutputTokenSpace, getModelCapabilities } from '../common/modelCapabilities.js';
|
||||
import { reParsedToolXMLString, chat_systemMessage, ToolName } from '../common/prompt/prompts.js';
|
||||
import { reParsedToolXMLString, chat_systemMessage } from '../common/prompt/prompts.js';
|
||||
import { AnthropicLLMChatMessage, AnthropicReasoning, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { ChatMode, FeatureName, ModelSelection, ProviderName } from '../common/voidSettingsTypes.js';
|
||||
|
|
@ -16,6 +16,8 @@ import { ITerminalToolService } from './terminalToolService.js';
|
|||
import { IVoidModelService } from '../common/voidModelService.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { EndOfLinePreference } from '../../../../editor/common/model.js';
|
||||
import { ToolName } from '../common/toolsServiceTypes.js';
|
||||
import { IMCPService } from '../common/mcpService.js';
|
||||
|
||||
export const EMPTY_MESSAGE = '(empty message)'
|
||||
|
||||
|
|
@ -455,8 +457,8 @@ const prepareGeminiMessages = (messages: AnthropicLLMChatMessage[]) => {
|
|||
return { text: c.text }
|
||||
}
|
||||
else if (c.type === 'tool_use') {
|
||||
latestToolName = c.name as ToolName
|
||||
return { functionCall: { id: c.id, name: c.name as ToolName, args: c.input } }
|
||||
latestToolName = c.name
|
||||
return { functionCall: { id: c.id, name: c.name, args: c.input } }
|
||||
}
|
||||
else return null
|
||||
}).filter(m => !!m)
|
||||
|
|
@ -538,6 +540,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
|
|||
@ITerminalToolService private readonly terminalToolService: ITerminalToolService,
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
@IVoidModelService private readonly voidModelService: IVoidModelService,
|
||||
@IMCPService private readonly mcpService: IMCPService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
|
@ -587,8 +590,10 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
|
|||
|
||||
const includeXMLToolDefinitions = !specialToolFormat
|
||||
|
||||
const mcpTools = this.mcpService.getMCPTools()
|
||||
|
||||
const persistentTerminalIDs = this.terminalToolService.listPersistentTerminalIds()
|
||||
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, persistentTerminalIDs, chatMode, includeXMLToolDefinitions })
|
||||
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, persistentTerminalIDs, chatMode, mcpTools, includeXMLToolDefinitions })
|
||||
return systemMessage
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { ChatMarkdownRender, ChatMessageLocation, getApplyBoxId } from '../markd
|
|||
import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||
import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js';
|
||||
import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch, VoidDiffEditor } from '../util/inputs.js';
|
||||
import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js';
|
||||
import { PastThreadsList } from './SidebarThreadSelector.js';
|
||||
import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
|
||||
|
|
@ -24,11 +24,11 @@ import { WarningBox } from '../void-settings-tsx/WarningBox.js';
|
|||
import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js';
|
||||
import { AlertTriangle, File, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag, Copy as CopyIcon, Info, CirclePlus, Ellipsis, CircleEllipsis, Folder, ALargeSmall, TypeOutline, Text } from 'lucide-react';
|
||||
import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js';
|
||||
import { approvalTypeOfToolName, LintErrorItem, ToolApprovalType, toolApprovalTypes, ToolCallParams } from '../../../../common/toolsServiceTypes.js';
|
||||
import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, BuiltinToolName, ToolName, LintErrorItem, ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js';
|
||||
import { CopyButton, EditToolAcceptRejectButtonsHTML, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyStreamState, useEditToolStreamState } from '../markdown/ApplyBlockHoverButtons.js';
|
||||
import { IsRunningType } from '../../../chatThreadService.js';
|
||||
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
|
||||
import { MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME, ToolName, toolNames } from '../../../../common/prompt/prompts.js';
|
||||
import { builtinToolNames, isABuiltinToolName, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME } from '../../../../common/prompt/prompts.js';
|
||||
import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js';
|
||||
import ErrorBoundary from './ErrorBoundary.js';
|
||||
import { ToolApprovalTypeSwitch } from '../void-settings-tsx/Settings.js';
|
||||
|
|
@ -36,6 +36,7 @@ import { ToolApprovalTypeSwitch } from '../void-settings-tsx/Settings.js';
|
|||
import { persistentTerminalNameOfId } from '../../../terminalToolService.js';
|
||||
|
||||
|
||||
|
||||
export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
|
|
@ -907,11 +908,14 @@ const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters<Res
|
|||
const desc1OnClick = () => voidOpenFileFn(params.uri, accessor)
|
||||
const componentParams: ToolHeaderParams = { title, desc1, desc1OnClick, desc1Info, isError, icon, isRejected, }
|
||||
|
||||
|
||||
const editToolType = toolMessage.name === 'edit_file' ? 'diff' : 'rewrite'
|
||||
if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') {
|
||||
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
|
||||
<EditToolChildren
|
||||
uri={params.uri}
|
||||
code={content}
|
||||
type={editToolType}
|
||||
/>
|
||||
</ToolChildrenWrapper>
|
||||
// JumpToFileButton removed in favor of FileLinkText
|
||||
|
|
@ -936,6 +940,7 @@ const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters<Res
|
|||
<EditToolChildren
|
||||
uri={params.uri}
|
||||
code={content}
|
||||
type={editToolType}
|
||||
/>
|
||||
</ToolChildrenWrapper>
|
||||
|
||||
|
|
@ -1388,7 +1393,7 @@ const loadingTitleWrapper = (item: React.ReactNode): React.ReactNode => {
|
|||
</span>
|
||||
}
|
||||
|
||||
const titleOfToolName = {
|
||||
const titleOfBuiltinToolName = {
|
||||
'read_file': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') },
|
||||
'ls_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') },
|
||||
'get_dir_tree': { done: 'Inspected folder tree', proposed: 'Inspect folder tree', running: loadingTitleWrapper('Inspecting folder tree') },
|
||||
|
|
@ -1406,21 +1411,42 @@ const titleOfToolName = {
|
|||
|
||||
'read_lint_errors': { done: `Read lint errors`, proposed: 'Read lint errors', running: loadingTitleWrapper('Reading lint errors') },
|
||||
'search_in_file': { done: 'Searched in file', proposed: 'Search in file', running: loadingTitleWrapper('Searching in file') },
|
||||
} as const satisfies Record<ToolName, { done: any, proposed: any, running: any }>
|
||||
} as const satisfies Record<BuiltinToolName, { done: any, proposed: any, running: any }>
|
||||
|
||||
|
||||
const getTitle = (toolMessage: Pick<ChatMessage & { role: 'tool' }, 'name' | 'type'>): React.ReactNode => {
|
||||
const getTitle = (toolMessage: Pick<ChatMessage & { role: 'tool' }, 'name' | 'type' | 'mcpServerName'>): React.ReactNode => {
|
||||
const t = toolMessage
|
||||
if (!toolNames.includes(t.name as ToolName)) return t.name // good measure
|
||||
|
||||
const toolName = t.name as ToolName
|
||||
if (t.type === 'success') return titleOfToolName[toolName].done
|
||||
if (t.type === 'running_now') return titleOfToolName[toolName].running
|
||||
return titleOfToolName[toolName].proposed
|
||||
// non-built-in title
|
||||
if (!builtinToolNames.includes(t.name as BuiltinToolName)) {
|
||||
// descriptor of Running or Ran etc
|
||||
const descriptor =
|
||||
t.type === 'success' ? 'Called'
|
||||
: t.type === 'running_now' ? 'Calling'
|
||||
: t.type === 'tool_request' ? 'Call'
|
||||
: t.type === 'rejected' ? 'Call'
|
||||
: t.type === 'invalid_params' ? 'Call'
|
||||
: t.type === 'tool_error' ? 'Call'
|
||||
: 'Call'
|
||||
|
||||
|
||||
const title = `${descriptor} ${toolMessage.mcpServerName || 'MCP'}`
|
||||
if (t.type === 'running_now' || t.type === 'tool_request')
|
||||
return loadingTitleWrapper(title)
|
||||
return title
|
||||
}
|
||||
|
||||
// built-in title
|
||||
else {
|
||||
const toolName = t.name as BuiltinToolName
|
||||
if (t.type === 'success') return titleOfBuiltinToolName[toolName].done
|
||||
if (t.type === 'running_now') return titleOfBuiltinToolName[toolName].running
|
||||
return titleOfBuiltinToolName[toolName].proposed
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined, accessor: ReturnType<typeof useAccessor>): {
|
||||
const toolNameToDesc = (toolName: BuiltinToolName, _toolParams: BuiltinToolCallParams[BuiltinToolName] | undefined, accessor: ReturnType<typeof useAccessor>): {
|
||||
desc1: React.ReactNode,
|
||||
desc1Info?: string,
|
||||
} => {
|
||||
|
|
@ -1431,95 +1457,95 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName
|
|||
|
||||
const x = {
|
||||
'read_file': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['read_file']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['read_file']
|
||||
return {
|
||||
desc1: getBasename(toolParams.uri.fsPath),
|
||||
desc1Info: getRelative(toolParams.uri, accessor),
|
||||
};
|
||||
},
|
||||
'ls_dir': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['ls_dir']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['ls_dir']
|
||||
return {
|
||||
desc1: getFolderName(toolParams.uri.fsPath),
|
||||
desc1Info: getRelative(toolParams.uri, accessor),
|
||||
};
|
||||
},
|
||||
'search_pathnames_only': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['search_pathnames_only']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['search_pathnames_only']
|
||||
return {
|
||||
desc1: `"${toolParams.query}"`,
|
||||
}
|
||||
},
|
||||
'search_for_files': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['search_for_files']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['search_for_files']
|
||||
return {
|
||||
desc1: `"${toolParams.query}"`,
|
||||
}
|
||||
},
|
||||
'search_in_file': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['search_in_file'];
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['search_in_file'];
|
||||
return {
|
||||
desc1: `"${toolParams.query}"`,
|
||||
desc1Info: getRelative(toolParams.uri, accessor),
|
||||
};
|
||||
},
|
||||
'create_file_or_folder': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['create_file_or_folder']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['create_file_or_folder']
|
||||
return {
|
||||
desc1: toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) ?? '/' : getBasename(toolParams.uri.fsPath),
|
||||
desc1Info: getRelative(toolParams.uri, accessor),
|
||||
}
|
||||
},
|
||||
'delete_file_or_folder': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['delete_file_or_folder']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['delete_file_or_folder']
|
||||
return {
|
||||
desc1: toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) ?? '/' : getBasename(toolParams.uri.fsPath),
|
||||
desc1Info: getRelative(toolParams.uri, accessor),
|
||||
}
|
||||
},
|
||||
'rewrite_file': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['rewrite_file']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['rewrite_file']
|
||||
return {
|
||||
desc1: getBasename(toolParams.uri.fsPath),
|
||||
desc1Info: getRelative(toolParams.uri, accessor),
|
||||
}
|
||||
},
|
||||
'edit_file': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['edit_file']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['edit_file']
|
||||
return {
|
||||
desc1: getBasename(toolParams.uri.fsPath),
|
||||
desc1Info: getRelative(toolParams.uri, accessor),
|
||||
}
|
||||
},
|
||||
'run_command': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['run_command']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['run_command']
|
||||
return {
|
||||
desc1: `"${toolParams.command}"`,
|
||||
}
|
||||
},
|
||||
'run_persistent_command': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['run_persistent_command']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['run_persistent_command']
|
||||
return {
|
||||
desc1: `"${toolParams.command}"`,
|
||||
}
|
||||
},
|
||||
'open_persistent_terminal': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['open_persistent_terminal']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['open_persistent_terminal']
|
||||
return { desc1: '' }
|
||||
},
|
||||
'kill_persistent_terminal': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['kill_persistent_terminal']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['kill_persistent_terminal']
|
||||
return { desc1: toolParams.persistentTerminalId }
|
||||
},
|
||||
'get_dir_tree': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['get_dir_tree']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['get_dir_tree']
|
||||
return {
|
||||
desc1: getFolderName(toolParams.uri.fsPath) ?? '/',
|
||||
desc1Info: getRelative(toolParams.uri, accessor),
|
||||
}
|
||||
},
|
||||
'read_lint_errors': () => {
|
||||
const toolParams = _toolParams as ToolCallParams['read_lint_errors']
|
||||
const toolParams = _toolParams as BuiltinToolCallParams['read_lint_errors']
|
||||
return {
|
||||
desc1: getBasename(toolParams.uri.fsPath),
|
||||
desc1Info: getRelative(toolParams.uri, accessor),
|
||||
|
|
@ -1590,9 +1616,9 @@ const ToolRequestAcceptRejectButtons = ({ toolName }: { toolName: ToolName }) =>
|
|||
</button>
|
||||
)
|
||||
|
||||
const approvalType = approvalTypeOfToolName[toolName]
|
||||
const approvalType = isABuiltinToolName(toolName) ? approvalTypeOfBuiltinToolName[toolName] : 'mcp-tools'
|
||||
const approvalToggle = approvalType ? <div key={approvalType} className="flex items-center ml-2 gap-x-1">
|
||||
<ToolApprovalTypeSwitch size='xs' approvalType={approvalType} desc='Auto-approve' />
|
||||
<ToolApprovalTypeSwitch size='xs' approvalType={approvalType} desc={`Auto-approve ${approvalType}`} />
|
||||
</div> : null
|
||||
|
||||
return <div className="flex gap-2 mx-0.5 items-center">
|
||||
|
|
@ -1604,7 +1630,7 @@ const ToolRequestAcceptRejectButtons = ({ toolName }: { toolName: ToolName }) =>
|
|||
|
||||
export const ToolChildrenWrapper = ({ children, className }: { children: React.ReactNode, className?: string }) => {
|
||||
return <div className={`${className ? className : ''} cursor-default select-none`}>
|
||||
<div className='px-2 min-w-full'>
|
||||
<div className='px-2 min-w-full overflow-hidden'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1633,12 +1659,18 @@ export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }:
|
|||
|
||||
|
||||
|
||||
const EditToolChildren = ({ uri, code }: { uri: URI | undefined, code: string }) => {
|
||||
const EditToolChildren = ({ uri, code, type }: { uri: URI | undefined, code: string, type: 'diff' | 'rewrite' }) => {
|
||||
|
||||
const content = type === 'diff' ?
|
||||
<VoidDiffEditor uri={uri} searchReplaceBlocks={code} />
|
||||
: <ChatMarkdownRender string={`\`\`\`\n${code}\n\`\`\``} codeURI={uri} chatMessageLocation={undefined} />
|
||||
|
||||
return <div className='!select-text cursor-auto'>
|
||||
<SmallProseWrapper>
|
||||
<ChatMarkdownRender string={code} codeURI={uri} chatMessageLocation={undefined} />
|
||||
{content}
|
||||
</SmallProseWrapper>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1689,9 +1721,9 @@ const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr, toolName, threadId }:
|
|||
|
||||
|
||||
|
||||
const InvalidTool = ({ toolName, message }: { toolName: ToolName, message: string }) => {
|
||||
const InvalidTool = ({ toolName, message, mcpServerName }: { toolName: ToolName, message: string, mcpServerName: string | undefined }) => {
|
||||
const accessor = useAccessor()
|
||||
const title = getTitle({ name: toolName, type: 'invalid_params' })
|
||||
const title = getTitle({ name: toolName, type: 'invalid_params', mcpServerName })
|
||||
const desc1 = 'Invalid parameters'
|
||||
const icon = null
|
||||
const isError = true
|
||||
|
|
@ -1705,9 +1737,9 @@ const InvalidTool = ({ toolName, message }: { toolName: ToolName, message: strin
|
|||
return <ToolHeaderWrapper {...componentParams} />
|
||||
}
|
||||
|
||||
const CanceledTool = ({ toolName }: { toolName: ToolName }) => {
|
||||
const CanceledTool = ({ toolName, mcpServerName }: { toolName: ToolName, mcpServerName: string | undefined }) => {
|
||||
const accessor = useAccessor()
|
||||
const title = getTitle({ name: toolName, type: 'rejected' })
|
||||
const title = getTitle({ name: toolName, type: 'rejected', mcpServerName })
|
||||
const desc1 = ''
|
||||
const icon = null
|
||||
const isRejected = true
|
||||
|
|
@ -1819,9 +1851,61 @@ const CommandTool = ({ toolMessage, type, threadId }: { threadId: string } & ({
|
|||
</>
|
||||
}
|
||||
|
||||
type WrapperProps<T extends ToolName> = { toolMessage: Exclude<ToolMessage<T>, { type: 'invalid_params' }>, messageIdx: number, threadId: string }
|
||||
const MCPToolWrapper = ({ toolMessage }: WrapperProps<string>) => {
|
||||
const accessor = useAccessor()
|
||||
const mcpService = accessor.get('IMCPService')
|
||||
|
||||
type ResultWrapper<T extends ToolName> = (props: { toolMessage: Exclude<ToolMessage<T>, { type: 'invalid_params' }>, messageIdx: number, threadId: string }) => React.ReactNode
|
||||
const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>, } } = {
|
||||
const title = getTitle(toolMessage)
|
||||
const desc1 = toolMessage.name
|
||||
const icon = null
|
||||
|
||||
|
||||
if (toolMessage.type === 'running_now') return null // do not show running
|
||||
|
||||
const isError = false
|
||||
const isRejected = toolMessage.type === 'rejected'
|
||||
const { rawParams, params } = toolMessage
|
||||
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected, }
|
||||
|
||||
const paramsStr = JSON.stringify(params, null, 2)
|
||||
componentParams.desc2 = <CopyButton codeStr={paramsStr} toolTipName={`Copy inputs: ${paramsStr}`} />
|
||||
|
||||
componentParams.info = !toolMessage.mcpServerName ? 'MCP tool not found' : undefined
|
||||
|
||||
// Add copy inputs button in desc2
|
||||
|
||||
|
||||
if (toolMessage.type === 'success' || toolMessage.type === 'tool_request') {
|
||||
const { result } = toolMessage
|
||||
const resultStr = result ? mcpService.stringifyResult(result) : 'null'
|
||||
componentParams.children = <ToolChildrenWrapper>
|
||||
<SmallProseWrapper>
|
||||
<ChatMarkdownRender
|
||||
string={`\`\`\`json\n${resultStr}\n\`\`\``}
|
||||
chatMessageLocation={undefined}
|
||||
isApplyEnabled={false}
|
||||
isLinkDetectionEnabled={true}
|
||||
/>
|
||||
</SmallProseWrapper>
|
||||
</ToolChildrenWrapper>
|
||||
}
|
||||
else if (toolMessage.type === 'tool_error') {
|
||||
const { result } = toolMessage
|
||||
componentParams.bottomChildren = <BottomChildren title='Error'>
|
||||
<CodeChildren>
|
||||
{result}
|
||||
</CodeChildren>
|
||||
</BottomChildren>
|
||||
}
|
||||
|
||||
return <ToolHeaderWrapper {...componentParams} />
|
||||
|
||||
}
|
||||
|
||||
type ResultWrapper<T extends ToolName> = (props: WrapperProps<T>) => React.ReactNode
|
||||
|
||||
const builtinToolNameToComponent: { [T in BuiltinToolName]: { resultWrapper: ResultWrapper<T>, } } = {
|
||||
'read_file': {
|
||||
resultWrapper: ({ toolMessage }) => {
|
||||
const accessor = useAccessor()
|
||||
|
|
@ -2257,12 +2341,12 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
|||
},
|
||||
'rewrite_file': {
|
||||
resultWrapper: (params) => {
|
||||
return <EditTool {...params} content={`${'```\n'}${params.toolMessage.params.newContent}${'\n```'}`} />
|
||||
return <EditTool {...params} content={params.toolMessage.params.newContent} />
|
||||
}
|
||||
},
|
||||
'edit_file': {
|
||||
resultWrapper: (params) => {
|
||||
return <EditTool {...params} content={`${'```\n'}${params.toolMessage.params.searchReplaceBlocks}${'\n```'}`} />
|
||||
return <EditTool {...params} content={params.toolMessage.params.searchReplaceBlocks} />
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -2442,11 +2526,15 @@ const _ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, me
|
|||
|
||||
if (chatMessage.type === 'invalid_params') {
|
||||
return <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
||||
<InvalidTool toolName={chatMessage.name} message={chatMessage.content} />
|
||||
<InvalidTool toolName={chatMessage.name} message={chatMessage.content} mcpServerName={chatMessage.mcpServerName} />
|
||||
</div>
|
||||
}
|
||||
|
||||
const ToolResultWrapper = toolNameToComponent[chatMessage.name]?.resultWrapper as ResultWrapper<ToolName>
|
||||
const toolName = chatMessage.name
|
||||
const isBuiltInTool = isABuiltinToolName(toolName)
|
||||
const ToolResultWrapper = isBuiltInTool ? builtinToolNameToComponent[toolName]?.resultWrapper as ResultWrapper<ToolName>
|
||||
: MCPToolWrapper as ResultWrapper<ToolName>
|
||||
|
||||
if (ToolResultWrapper)
|
||||
return <>
|
||||
<div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
||||
|
|
@ -2466,7 +2554,7 @@ const _ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, me
|
|||
|
||||
else if (role === 'interrupted_streaming_tool') {
|
||||
return <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
||||
<CanceledTool toolName={chatMessage.name} />
|
||||
<CanceledTool toolName={chatMessage.name} mcpServerName={chatMessage.mcpServerName} />
|
||||
</div>
|
||||
}
|
||||
|
||||
|
|
@ -2746,12 +2834,13 @@ const CommandBarInChat = () => {
|
|||
|
||||
const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => {
|
||||
|
||||
if (!isABuiltinToolName(toolCallSoFar.name)) return null
|
||||
|
||||
const accessor = useAccessor()
|
||||
|
||||
const uri = toolCallSoFar.rawParams.uri ? URI.file(toolCallSoFar.rawParams.uri) : undefined
|
||||
|
||||
const title = titleOfToolName[toolCallSoFar.name].proposed
|
||||
const title = titleOfBuiltinToolName[toolCallSoFar.name].proposed
|
||||
|
||||
const uriDone = toolCallSoFar.doneParams.includes('uri')
|
||||
const desc1 = <span className='flex items-center'>
|
||||
|
|
@ -2772,12 +2861,11 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) =>
|
|||
<EditToolChildren
|
||||
uri={uri}
|
||||
code={toolCallSoFar.rawParams.search_replace_blocks ?? toolCallSoFar.rawParams.new_content ?? ''}
|
||||
type={'rewrite'} // as it streams, show in rewrite format, don't make a diff editor
|
||||
/>
|
||||
<IconLoading />
|
||||
</ToolHeaderWrapper>
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,11 @@ import { URI } from '../../../../../../../base/common/uri.js';
|
|||
import { getBasename, getFolderName } from '../sidebar-tsx/SidebarChat.js';
|
||||
import { ChevronRight, File, Folder, FolderClosed, LucideProps } from 'lucide-react';
|
||||
import { StagingSelectionItem } from '../../../../common/chatThreadServiceTypes.js';
|
||||
import { DiffEditorWidget } from '../../../../../../../editor/browser/widget/diffEditor/diffEditorWidget.js';
|
||||
import { extractSearchReplaceBlocks, ExtractedSearchReplaceBlock } from '../../../../common/helpers/extractCodeFromResult.js';
|
||||
import { IAccessibilitySignalService } from '../../../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
|
||||
import { IEditorProgressService } from '../../../../../../../platform/progress/common/progress.js';
|
||||
import { detectLanguage } from '../../../../common/helpers/languageHelpers.js';
|
||||
|
||||
|
||||
// type guard
|
||||
|
|
@ -951,11 +956,11 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
|
|||
|
||||
const contextViewProvider = accessor.get('IContextViewService')
|
||||
return <WidgetComponent
|
||||
ctor={InputBox}
|
||||
className='
|
||||
bg-void-bg-1
|
||||
@@void-force-child-placeholder-void-fg-1
|
||||
'
|
||||
ctor={InputBox}
|
||||
propsFn={useCallback((container) => [
|
||||
container,
|
||||
contextViewProvider,
|
||||
|
|
@ -991,8 +996,7 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
|
|||
inputBoxRef.current = instance;
|
||||
|
||||
return disposables
|
||||
}, [onChangeText, onCreateInstance, inputBoxRef])
|
||||
}
|
||||
}, [onChangeText, onCreateInstance, inputBoxRef])}
|
||||
/>
|
||||
};
|
||||
|
||||
|
|
@ -1841,3 +1845,154 @@ export const VoidButtonBgDarken = ({ children, disabled, onClick, className }: {
|
|||
// };
|
||||
|
||||
|
||||
|
||||
|
||||
const SingleDiffEditor = ({ block, lang }: { block: ExtractedSearchReplaceBlock, lang: string | undefined }) => {
|
||||
const accessor = useAccessor();
|
||||
const modelService = accessor.get('IModelService');
|
||||
const instantiationService = accessor.get('IInstantiationService');
|
||||
const languageService = accessor.get('ILanguageService');
|
||||
|
||||
const languageSelection = useMemo(() => languageService.createById(lang), [lang, languageService]);
|
||||
|
||||
// Create models for original and modified
|
||||
const originalModel = useMemo(() =>
|
||||
modelService.createModel(block.orig, languageSelection),
|
||||
[block.orig, languageSelection, modelService]
|
||||
);
|
||||
const modifiedModel = useMemo(() =>
|
||||
modelService.createModel(block.final, languageSelection),
|
||||
[block.final, languageSelection, modelService]
|
||||
);
|
||||
|
||||
// Clean up models on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
originalModel.dispose();
|
||||
modifiedModel.dispose();
|
||||
};
|
||||
}, [originalModel, modifiedModel]);
|
||||
|
||||
// Imperatively mount the DiffEditorWidget
|
||||
const divRef = useRef<HTMLDivElement | null>(null);
|
||||
const editorRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!divRef.current) return;
|
||||
// Create the diff editor instance
|
||||
const editor = instantiationService.createInstance(
|
||||
DiffEditorWidget,
|
||||
divRef.current,
|
||||
{
|
||||
automaticLayout: true,
|
||||
readOnly: true,
|
||||
renderSideBySide: true,
|
||||
minimap: { enabled: false },
|
||||
lineNumbers: 'off',
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'auto',
|
||||
verticalScrollbarSize: 0,
|
||||
horizontalScrollbarSize: 8,
|
||||
alwaysConsumeMouseWheel: false,
|
||||
ignoreHorizontalScrollbarInContentHeight: true,
|
||||
},
|
||||
hover: { enabled: false },
|
||||
folding: false,
|
||||
selectionHighlight: false,
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerLanes: 0,
|
||||
hideCursorInOverviewRuler: true,
|
||||
overviewRulerBorder: false,
|
||||
glyphMargin: false,
|
||||
stickyScroll: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
renderGutterMenu: false,
|
||||
renderIndicators: false,
|
||||
},
|
||||
{ originalEditor: { isSimpleWidget: true }, modifiedEditor: { isSimpleWidget: true } }
|
||||
);
|
||||
editor.setModel({ original: originalModel, modified: modifiedModel });
|
||||
|
||||
// Calculate the height based on content
|
||||
const updateHeight = () => {
|
||||
const contentHeight = Math.max(
|
||||
originalModel.getLineCount() * 19, // approximate line height
|
||||
modifiedModel.getLineCount() * 19
|
||||
) + 19 * 2 + 1; // add padding
|
||||
|
||||
// Set reasonable min/max heights
|
||||
const height = Math.min(Math.max(contentHeight, 100), 300);
|
||||
if (divRef.current) {
|
||||
divRef.current.style.height = `${height}px`;
|
||||
editor.layout();
|
||||
}
|
||||
};
|
||||
|
||||
updateHeight();
|
||||
editorRef.current = editor;
|
||||
|
||||
// Update height when content changes
|
||||
const disposable1 = originalModel.onDidChangeContent(() => updateHeight());
|
||||
const disposable2 = modifiedModel.onDidChangeContent(() => updateHeight());
|
||||
|
||||
return () => {
|
||||
disposable1.dispose();
|
||||
disposable2.dispose();
|
||||
editor.dispose();
|
||||
editorRef.current = null;
|
||||
};
|
||||
}, [originalModel, modifiedModel, instantiationService]);
|
||||
|
||||
return (
|
||||
<div className="w-full bg-void-bg-3 @@bg-editor-style-override" ref={divRef} />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* ToolDiffEditor mounts a native VSCode DiffEditorWidget to show a diff between original and modified code blocks.
|
||||
* Props:
|
||||
* - uri: URI of the file (for language detection, etc)
|
||||
* - searchReplaceBlocks: string in search/replace format (from LLM)
|
||||
* - language?: string (optional, fallback to 'plaintext')
|
||||
*/
|
||||
export const VoidDiffEditor = ({ uri, searchReplaceBlocks, language }: { uri?: any, searchReplaceBlocks: string, language?: string }) => {
|
||||
const accessor = useAccessor();
|
||||
const languageService = accessor.get('ILanguageService');
|
||||
|
||||
// Extract all blocks
|
||||
const blocks = extractSearchReplaceBlocks(searchReplaceBlocks);
|
||||
|
||||
// Use detectLanguage for language detection if not provided
|
||||
let lang = language;
|
||||
if (!lang && blocks.length > 0) {
|
||||
lang = detectLanguage(languageService, { uri: uri ?? null, fileContents: blocks[0].orig });
|
||||
}
|
||||
|
||||
// If no blocks, show empty state
|
||||
if (blocks.length === 0) {
|
||||
return <div className="w-full p-4 text-void-fg-4 text-sm">No changes found</div>;
|
||||
}
|
||||
|
||||
// Display all blocks
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
{blocks.map((block, index) => (
|
||||
<div key={index} className="w-full">
|
||||
{blocks.length > 1 && (
|
||||
<div className="text-void-fg-4 text-xs mb-1 px-1">
|
||||
Change {index + 1} of {blocks.length}
|
||||
</div>
|
||||
)}
|
||||
<SingleDiffEditor block={block} lang={lang} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
||||
import { MCPUserState, RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||
import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
|
||||
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
|
||||
|
|
@ -51,6 +51,7 @@ import { IConvertToLLMMessageService } from '../../../convertToLLMMessageService
|
|||
import { ITerminalService } from '../../../../../terminal/browser/terminal.js'
|
||||
import { ISearchService } from '../../../../../../services/search/common/search.js'
|
||||
import { IExtensionManagementService } from '../../../../../../../platform/extensionManagement/common/extensionManagement.js'
|
||||
import { IMCPService } from '../../../../common/mcpService.js';
|
||||
|
||||
|
||||
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
|
||||
|
|
@ -78,6 +79,8 @@ const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) =>
|
|||
const commandBarURIStateListeners: Set<(uri: URI) => void> = new Set();
|
||||
const activeURIListeners: Set<(uri: URI | null) => void> = new Set();
|
||||
|
||||
const mcpListeners: Set<() => void> = new Set()
|
||||
|
||||
|
||||
// must call this before you can use any of the hooks below
|
||||
// this should only be called ONCE! this is the only place you don't need to dispose onDidChange. If you use state.onDidChange anywhere else, make sure to dispose it!
|
||||
|
|
@ -95,9 +98,10 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
|||
editCodeService: accessor.get(IEditCodeService),
|
||||
voidCommandBarService: accessor.get(IVoidCommandBarService),
|
||||
modelService: accessor.get(IModelService),
|
||||
mcpService: accessor.get(IMCPService),
|
||||
}
|
||||
|
||||
const { settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService } = stateServices
|
||||
const { settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService, mcpService } = stateServices
|
||||
|
||||
|
||||
|
||||
|
|
@ -164,6 +168,11 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
|||
})
|
||||
)
|
||||
|
||||
disposables.push(
|
||||
mcpService.onDidChangeState(() => {
|
||||
mcpListeners.forEach(l => l())
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
return disposables
|
||||
|
|
@ -215,6 +224,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
|||
ITerminalService: accessor.get(ITerminalService),
|
||||
IExtensionManagementService: accessor.get(IExtensionManagementService),
|
||||
IExtensionTransferService: accessor.get(IExtensionTransferService),
|
||||
IMCPService: accessor.get(IMCPService),
|
||||
|
||||
} as const
|
||||
return reactAccessor
|
||||
|
|
@ -376,3 +386,16 @@ export const useActiveURI = () => {
|
|||
|
||||
|
||||
|
||||
|
||||
export const useMCPServiceState = () => {
|
||||
const accessor = useAccessor()
|
||||
const mcpService = accessor.get('IMCPService')
|
||||
const [s, ss] = useState(mcpService.state)
|
||||
useEffect(() => {
|
||||
const listener = () => { ss(mcpService.state) }
|
||||
mcpListeners.add(listener);
|
||||
return () => { mcpListeners.delete(listener) };
|
||||
}, []);
|
||||
return s
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import { ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsSer
|
|||
import Severity from '../../../../../../../base/common/severity.js'
|
||||
import { getModelCapabilities, modelOverrideKeys, ModelOverrides } from '../../../../common/modelCapabilities.js';
|
||||
import { TransferEditorType, TransferFilesInfo } from '../../../extensionTransferTypes.js';
|
||||
import { MCPServer } from '../../../../common/mcpServiceTypes.js';
|
||||
import { useMCPServiceState } from '../util/services.js';
|
||||
|
||||
const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => {
|
||||
|
||||
|
|
@ -454,7 +456,7 @@ export const ModelDump = ({ filteredProviders }: { filteredProviders?: ProviderN
|
|||
{/* left part is width:full */}
|
||||
<div className={`flex flex-grow items-center gap-4`}>
|
||||
<span className='w-full max-w-32'>{isNewProviderName ? providerTitle : ''}</span>
|
||||
<span className='w-fit truncate'>{modelName}</span>
|
||||
<span className='w-fit max-w-[400px] truncate'>{modelName}</span>
|
||||
</div>
|
||||
|
||||
{/* right part is anything that fits */}
|
||||
|
|
@ -493,7 +495,15 @@ export const ModelDump = ({ filteredProviders }: { filteredProviders?: ProviderN
|
|||
|
||||
{/* X button */}
|
||||
<div className={`w-5 flex items-center justify-center`}>
|
||||
{type === 'default' || type === 'autodetected' ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName); }}><X className="size-4" /></button>}
|
||||
{type === 'default' || type === 'autodetected' ? null : <button
|
||||
onClick={() => { settingsStateService.deleteModel(providerName, modelName); }}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='right'
|
||||
data-tooltip-content='Delete'
|
||||
className={`${hasOverrides ? '' : 'opacity-0 group-hover:opacity-100'} transition-opacity`}
|
||||
>
|
||||
<X size={12} className="text-void-fg-3 opacity-50" />
|
||||
</button>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -898,6 +908,110 @@ export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }:
|
|||
|
||||
// full settings
|
||||
|
||||
// MCP Server component
|
||||
const MCPServerComponent = ({ name, server }: { name: string, server: MCPServer }) => {
|
||||
const accessor = useAccessor();
|
||||
const mcpService = accessor.get('IMCPService');
|
||||
|
||||
const voidSettings = useSettingsState()
|
||||
const isOn = voidSettings.mcpUserStateOfName[name]?.isOn
|
||||
|
||||
const removeUniquePrefix = (name: string) => name.split('_').slice(1).join('_')
|
||||
|
||||
return (
|
||||
<div className="border-b border-gray-800 bg-gray-300/10 py-4 rounded-lg ">
|
||||
<div className="flex items-center mx-4">
|
||||
{/* Status indicator */}
|
||||
<div className={`w-2 h-2 rounded-full mr-2
|
||||
${server.status === 'success' ? 'bg-green-500'
|
||||
: server.status === 'error' ? 'bg-red-500'
|
||||
: server.status === 'loading' ? 'bg-yellow-500'
|
||||
: server.status === 'offline' ? 'bg-gray-500'
|
||||
: ''}
|
||||
|
||||
`}></div>
|
||||
|
||||
{/* Server name */}
|
||||
<div className="text-sm font-medium mr-2">{name}</div>
|
||||
|
||||
{/* Power toggle switch */}
|
||||
<div className="ml-auto mb-2">
|
||||
<VoidSwitch
|
||||
value={isOn ?? false}
|
||||
size='sm'
|
||||
disabled={server.status === 'error'}
|
||||
onChange={() => mcpService.toggleServerIsOn(name, !isOn)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tools section */}
|
||||
<div className="mt-1 mx-4">
|
||||
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto pb-1">
|
||||
{isOn && (server.tools ?? []).length > 0 ? (
|
||||
(server.tools ?? []).map((tool: { name: string; description?: string }) => (
|
||||
<span
|
||||
key={tool.name}
|
||||
className="px-2 py-0.5 bg-black/5 dark:bg-white/5 rounded text-xs"
|
||||
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content={tool.description || ''}
|
||||
data-tooltip-class-name='void-max-w-[300px]'
|
||||
>
|
||||
{removeUniquePrefix(tool.name)}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-gray-500">No tools available</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Command badge */}
|
||||
{isOn && server.command && (
|
||||
<div className="mt-2 mx-4">
|
||||
<div className="text-xs text-gray-400">Command:</div>
|
||||
<div className="px-2 py-1 bg-void-bg-3 text-xs font-mono overflow-x-auto whitespace-nowrap">
|
||||
{server.command}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error message if present */}
|
||||
{server.error && (<WarningBox className='ml-4' text={server.error} />)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Main component that renders the list of servers
|
||||
const MCPServersList = () => {
|
||||
const mcpServiceState = useMCPServiceState()
|
||||
|
||||
let content: React.ReactNode
|
||||
if (mcpServiceState.error) {
|
||||
content = <div className="text-red-500 text-sm font-medium">
|
||||
{mcpServiceState.error}
|
||||
</div>
|
||||
}
|
||||
else {
|
||||
const entries = Object.entries(mcpServiceState.mcpServerOfName)
|
||||
if (entries.length === 0) {
|
||||
content = <div className="text-red-500 text-sm font-medium">
|
||||
No servers found
|
||||
</div>
|
||||
}
|
||||
else {
|
||||
content = entries.map(([name, server]) => (
|
||||
<div className="py-2" key={name}>
|
||||
<MCPServerComponent name={name} server={server} />
|
||||
</div>
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
return content
|
||||
};
|
||||
|
||||
export const Settings = () => {
|
||||
const isDark = useIsDark()
|
||||
const accessor = useAccessor()
|
||||
|
|
@ -908,6 +1022,7 @@ export const Settings = () => {
|
|||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const notificationService = accessor.get('INotificationService')
|
||||
const mcpService = accessor.get('IMCPService')
|
||||
|
||||
const onDownload = (t: 'Chats' | 'Settings') => {
|
||||
let dataStr: string
|
||||
|
|
@ -1033,7 +1148,7 @@ export const Settings = () => {
|
|||
className='hover:brightness-110'
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-content='We recommend using the largest qwen2.5-coder model you can with Ollama (try qwen2.5-coder:3b).'
|
||||
data-tooltip-class-name='void-max-w-[20px]'
|
||||
data-tooltip-class-name='void-max-w-[300px]'
|
||||
>
|
||||
Only works with FIM models.*
|
||||
</span>
|
||||
|
|
@ -1233,6 +1348,7 @@ export const Settings = () => {
|
|||
|
||||
<div className='mt-12 max-w-[600px]'>
|
||||
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
|
||||
|
||||
<h4 className={`text-void-fg-3 mb-4`}>
|
||||
<ChatMarkdownRender inPTag={true} string={`
|
||||
System instructions to include with all AI requests.
|
||||
|
|
@ -1243,6 +1359,24 @@ Alternatively, place a \`.voidrules\` file in the root of your workspace.
|
|||
<AIInstructionsBox />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
<div className='mt-12 max-w-[600px]'>
|
||||
<h2 className='text-3xl mb-2'>MCP</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>
|
||||
<ChatMarkdownRender inPTag={true} string={`
|
||||
Use Model Context Protocol to provide Agent mode with more tools.
|
||||
`} chatMessageLocation={undefined} />
|
||||
</h4>
|
||||
<div>
|
||||
<VoidButtonBgDarken className='px-4 py-1 mb-2 w-full max-w-48' onClick={async () => { await mcpService.revealMCPConfigFile() }}>
|
||||
Add MCP Server
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ErrorBoundary>
|
||||
<MCPServersList />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ export const VoidTooltip = () => {
|
|||
padding: 0px 8px;
|
||||
border-radius: 6px;
|
||||
z-index: 999999;
|
||||
max-width: 300px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#void-tooltip {
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ registerAction2(class extends Action2 {
|
|||
const oldUI = await oldThread?.state.mountedInfo?.whenMounted
|
||||
|
||||
const oldSelns = oldThread?.state.stagingSelections
|
||||
const oldVal = oldUI?.textAreaRef.current?.value
|
||||
const oldVal = oldUI?.textAreaRef?.current?.value
|
||||
|
||||
// open and focus new thread
|
||||
chatThreadsService.openNewThread()
|
||||
|
|
|
|||
|
|
@ -346,20 +346,20 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
|||
await Promise.any([waitUntilDone, waitUntilInterrupt])
|
||||
.finally(() => disposables.forEach(d => d.dispose()))
|
||||
|
||||
|
||||
|
||||
// read result if timed out, since we didn't get it (could clean this code up but it's ok)
|
||||
if (resolveReason?.type === 'timeout') {
|
||||
const terminalId = isPersistent ? params.persistentTerminalId : params.terminalId
|
||||
result = await this.readTerminal(terminalId)
|
||||
}
|
||||
|
||||
if (!isPersistent) {
|
||||
interrupt()
|
||||
}
|
||||
|
||||
if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.')
|
||||
|
||||
// read result if timed out, since we didn't get it (could clean this code up but it's ok)
|
||||
if (resolveReason.type === 'timeout') {
|
||||
const terminalId = isPersistent ? params.persistentTerminalId : params.terminalId
|
||||
result = await this.readTerminal(terminalId)
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (!isPersistent) result = `$ ${command}\n${result}`
|
||||
result = removeAnsiEscapeCodes(result)
|
||||
// trim
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { QueryBuilder } from '../../../services/search/common/queryBuilder.js'
|
|||
import { ISearchService } from '../../../services/search/common/search.js'
|
||||
import { IEditCodeService } from './editCodeServiceInterface.js'
|
||||
import { ITerminalToolService } from './terminalToolService.js'
|
||||
import { LintErrorItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js'
|
||||
import { LintErrorItem, BuiltinToolCallParams, BuiltinToolResultType, BuiltinToolName } from '../common/toolsServiceTypes.js'
|
||||
import { IVoidModelService } from '../common/voidModelService.js'
|
||||
import { EndOfLinePreference } from '../../../../editor/common/model.js'
|
||||
import { IVoidCommandBarService } from './voidCommandBarService.js'
|
||||
|
|
@ -16,20 +16,15 @@ import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree
|
|||
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'
|
||||
import { timeout } from '../../../../base/common/async.js'
|
||||
import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js'
|
||||
import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_INACTIVE_TIME, ToolName } from '../common/prompt/prompts.js'
|
||||
import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_INACTIVE_TIME } from '../common/prompt/prompts.js'
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js'
|
||||
import { generateUuid } from '../../../../base/common/uuid.js'
|
||||
|
||||
|
||||
// tool use for AI
|
||||
|
||||
|
||||
|
||||
|
||||
type ValidateParams = { [T in ToolName]: (p: RawToolParamsObj) => ToolCallParams[T] }
|
||||
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T] | Promise<ToolResultType[T]>, interruptTool?: () => void }> }
|
||||
type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited<ToolResultType[T]>) => string }
|
||||
|
||||
type ValidateBuiltinParams = { [T in BuiltinToolName]: (p: RawToolParamsObj) => BuiltinToolCallParams[T] }
|
||||
type CallBuiltinTool = { [T in BuiltinToolName]: (p: BuiltinToolCallParams[T]) => Promise<{ result: BuiltinToolResultType[T] | Promise<BuiltinToolResultType[T]>, interruptTool?: () => void }> }
|
||||
type BuiltinToolResultToString = { [T in BuiltinToolName]: (p: BuiltinToolCallParams[T], result: Awaited<BuiltinToolResultType[T]>) => string }
|
||||
|
||||
|
||||
const isFalsy = (u: unknown) => {
|
||||
|
|
@ -110,9 +105,9 @@ const checkIfIsFolder = (uriStr: string) => {
|
|||
|
||||
export interface IToolsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
validateParams: ValidateParams;
|
||||
callTool: CallTool;
|
||||
stringOfResult: ToolResultToString;
|
||||
validateParams: ValidateBuiltinParams;
|
||||
callTool: CallBuiltinTool;
|
||||
stringOfResult: BuiltinToolResultToString;
|
||||
}
|
||||
|
||||
export const IToolsService = createDecorator<IToolsService>('ToolsService');
|
||||
|
|
@ -121,9 +116,9 @@ export class ToolsService implements IToolsService {
|
|||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
public validateParams: ValidateParams;
|
||||
public callTool: CallTool;
|
||||
public stringOfResult: ToolResultToString;
|
||||
public validateParams: ValidateBuiltinParams;
|
||||
public callTool: CallBuiltinTool;
|
||||
public stringOfResult: BuiltinToolResultToString;
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
|
|
@ -446,7 +441,6 @@ export class ToolsService implements IToolsService {
|
|||
await this.terminalToolService.killPersistentTerminal(persistentTerminalId)
|
||||
return { result: {} }
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -550,7 +544,6 @@ export class ToolsService implements IToolsService {
|
|||
kill_persistent_terminal: (params, _result) => {
|
||||
return `Successfully closed terminal "${params.persistentTerminalId}".`;
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,31 +5,32 @@
|
|||
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { VoidFileSnapshot } from './editCodeServiceTypes.js';
|
||||
import { ToolName } from './prompt/prompts.js';
|
||||
import { AnthropicReasoning, RawToolParamsObj } from './sendLLMMessageTypes.js';
|
||||
import { ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
|
||||
import { ToolCallParams, ToolName, ToolResult } from './toolsServiceTypes.js';
|
||||
|
||||
export type ToolMessage<T extends ToolName> = {
|
||||
role: 'tool';
|
||||
content: string; // give this result to LLM (string of value)
|
||||
id: string;
|
||||
rawParams: RawToolParamsObj;
|
||||
mcpServerName: string | undefined; // the server name at the time of the call
|
||||
} & (
|
||||
// in order of events:
|
||||
| { type: 'invalid_params', result: null, name: T, }
|
||||
|
||||
| { type: 'tool_request', result: null, name: T, params: ToolCallParams[T], } // params were validated, awaiting user
|
||||
| { type: 'tool_request', result: null, name: T, params: ToolCallParams<T>, } // params were validated, awaiting user
|
||||
|
||||
| { type: 'running_now', result: null, name: T, params: ToolCallParams[T], }
|
||||
| { type: 'running_now', result: null, name: T, params: ToolCallParams<T>, }
|
||||
|
||||
| { type: 'tool_error', result: string, name: T, params: ToolCallParams[T], } // error when tool was running
|
||||
| { type: 'success', result: Awaited<ToolResultType[T]>, name: T, params: ToolCallParams[T], }
|
||||
| { type: 'rejected', result: null, name: T, params: ToolCallParams[T] }
|
||||
| { type: 'tool_error', result: string, name: T, params: ToolCallParams<T>, } // error when tool was running
|
||||
| { type: 'success', result: Awaited<ToolResult<T>>, name: T, params: ToolCallParams<T>, }
|
||||
| { type: 'rejected', result: null, name: T, params: ToolCallParams<T> }
|
||||
) // user rejected
|
||||
|
||||
export type DecorativeCanceledTool = {
|
||||
role: 'interrupted_streaming_tool';
|
||||
name: ToolName;
|
||||
mcpServerName: string | undefined; // the server name at the time of the call
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta
|
|||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IFileService, IFileStat } from '../../../../platform/files/common/files.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
|
||||
import { ShallowDirectoryItem, BuiltinToolCallParams, BuiltinToolResultType } from './toolsServiceTypes.js';
|
||||
import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from './prompt/prompts.js';
|
||||
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ export const computeDirectoryTree1Deep = async (
|
|||
fileService: IFileService,
|
||||
rootURI: URI,
|
||||
pageNumber: number = 1,
|
||||
): Promise<ToolResultType['ls_dir']> => {
|
||||
): Promise<BuiltinToolResultType['ls_dir']> => {
|
||||
const stat = await fileService.resolve(rootURI, { resolveMetadata: false });
|
||||
if (!stat.isDirectory) {
|
||||
return { children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 };
|
||||
|
|
@ -107,7 +107,7 @@ export const computeDirectoryTree1Deep = async (
|
|||
};
|
||||
};
|
||||
|
||||
export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], result: ToolResultType['ls_dir']): string => {
|
||||
export const stringifyDirectoryTree1Deep = (params: BuiltinToolCallParams['ls_dir'], result: BuiltinToolResultType['ls_dir']): string => {
|
||||
if (!result.children) {
|
||||
return `Error: ${params.uri} is not a directory`;
|
||||
}
|
||||
|
|
|
|||
360
src/vs/workbench/contrib/void/common/mcpService.ts
Normal file
360
src/vs/workbench/contrib/void/common/mcpService.ts
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IPathService } from '../../../services/path/common/pathService.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { IProductService } from '../../../../platform/product/common/productService.js';
|
||||
import { VSBuffer } from '../../../../base/common/buffer.js';
|
||||
import { IChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
|
||||
import { MCPServerOfName, MCPConfigFileJSON, MCPServer, MCPToolCallParams, RawMCPToolCall, MCPServerEventResponse } from './mcpServiceTypes.js';
|
||||
import { Event, Emitter } from '../../../../base/common/event.js';
|
||||
import { InternalToolInfo } from './prompt/prompts.js';
|
||||
import { IVoidSettingsService } from './voidSettingsService.js';
|
||||
import { MCPUserStateOfName } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
type MCPServiceState = {
|
||||
mcpServerOfName: MCPServerOfName,
|
||||
error: string | undefined, // global parsing error
|
||||
}
|
||||
|
||||
export interface IMCPService {
|
||||
readonly _serviceBrand: undefined;
|
||||
revealMCPConfigFile(): Promise<void>;
|
||||
toggleServerIsOn(serverName: string, isOn: boolean): Promise<void>;
|
||||
|
||||
readonly state: MCPServiceState; // NOT persisted
|
||||
onDidChangeState: Event<void>;
|
||||
|
||||
getMCPTools(): InternalToolInfo[] | undefined;
|
||||
callMCPTool(toolData: MCPToolCallParams): Promise<{ result: RawMCPToolCall }>;
|
||||
stringifyResult(result: RawMCPToolCall): string
|
||||
}
|
||||
|
||||
export const IMCPService = createDecorator<IMCPService>('mcpConfigService');
|
||||
|
||||
|
||||
|
||||
const MCP_CONFIG_FILE_NAME = 'mcp.json';
|
||||
const MCP_CONFIG_SAMPLE = { mcpServers: {} }
|
||||
const MCP_CONFIG_SAMPLE_STRING = JSON.stringify(MCP_CONFIG_SAMPLE, null, 2);
|
||||
|
||||
|
||||
// export interface MCPCallToolOfToolName {
|
||||
// [toolName: string]: (params: any) => Promise<{
|
||||
// result: any | Promise<any>,
|
||||
// interruptTool?: () => void
|
||||
// }>;
|
||||
// }
|
||||
|
||||
|
||||
class MCPService extends Disposable implements IMCPService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
|
||||
private readonly channel: IChannel // MCPChannel
|
||||
|
||||
// list of MCP servers pulled from mcpChannel
|
||||
state: MCPServiceState = {
|
||||
mcpServerOfName: {},
|
||||
error: undefined,
|
||||
}
|
||||
|
||||
// Emitters for server events
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
public readonly onDidChangeState = this._onDidChangeState.event;
|
||||
|
||||
// private readonly _onLoadingServersChange = new Emitter<MCPServerEventLoadingParam>();
|
||||
// public readonly onLoadingServersChange = this._onLoadingServersChange.event;
|
||||
|
||||
constructor(
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IPathService private readonly pathService: IPathService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@IMainProcessService private readonly mainProcessService: IMainProcessService,
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
) {
|
||||
super();
|
||||
this.channel = this.mainProcessService.getChannel('void-channel-mcp')
|
||||
|
||||
|
||||
const onEvent = (e: MCPServerEventResponse) => {
|
||||
console.log('GOT EVENT', e)
|
||||
this._setMCPServerState(e.response.name, e.response.newServer)
|
||||
}
|
||||
this._register((this.channel.listen('onAdd_server') satisfies Event<MCPServerEventResponse>)(onEvent));
|
||||
this._register((this.channel.listen('onUpdate_server') satisfies Event<MCPServerEventResponse>)(onEvent));
|
||||
this._register((this.channel.listen('onDelete_server') satisfies Event<MCPServerEventResponse>)(onEvent));
|
||||
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
|
||||
private async _initialize() {
|
||||
try {
|
||||
await this.voidSettingsService.waitForInitState;
|
||||
|
||||
// Create .mcpConfig if it doesn't exist
|
||||
const mcpConfigUri = await this._getMCPConfigFilePath();
|
||||
const fileExists = await this._configFileExists(mcpConfigUri);
|
||||
if (!fileExists) {
|
||||
await this._createMCPConfigFile(mcpConfigUri);
|
||||
console.log('MCP Config file created:', mcpConfigUri.toString());
|
||||
}
|
||||
await this._addMCPConfigFileWatcher();
|
||||
await this._refreshMCPServers();
|
||||
} catch (error) {
|
||||
console.error('Error initializing MCPService:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly _setMCPServerState = async (serverName: string, newServer: MCPServer | undefined) => {
|
||||
if (newServer === undefined) {
|
||||
// Remove the server from the state
|
||||
const { [serverName]: removed, ...remainingServers } = this.state.mcpServerOfName;
|
||||
this.state = {
|
||||
...this.state,
|
||||
mcpServerOfName: remainingServers
|
||||
}
|
||||
} else {
|
||||
// Add or update the server
|
||||
this.state = {
|
||||
...this.state,
|
||||
mcpServerOfName: {
|
||||
...this.state.mcpServerOfName,
|
||||
[serverName]: newServer
|
||||
}
|
||||
}
|
||||
}
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
private readonly _setHasError = async (errMsg: string | undefined) => {
|
||||
this.state = {
|
||||
...this.state,
|
||||
error: errMsg,
|
||||
}
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
// Create the file/directory if it doesn't exist
|
||||
private async _createMCPConfigFile(mcpConfigUri: URI): Promise<void> {
|
||||
await this.fileService.createFile(mcpConfigUri.with({ path: mcpConfigUri.path }));
|
||||
const buffer = VSBuffer.fromString(MCP_CONFIG_SAMPLE_STRING);
|
||||
await this.fileService.writeFile(mcpConfigUri, buffer);
|
||||
}
|
||||
|
||||
|
||||
private async _addMCPConfigFileWatcher(): Promise<void> {
|
||||
const mcpConfigUri = await this._getMCPConfigFilePath();
|
||||
this._register(
|
||||
this.fileService.watch(mcpConfigUri)
|
||||
)
|
||||
|
||||
this._register(this.fileService.onDidFilesChange(async e => {
|
||||
if (!e.contains(mcpConfigUri)) return
|
||||
await this._refreshMCPServers();
|
||||
}));
|
||||
}
|
||||
|
||||
// Client-side functions
|
||||
|
||||
public async revealMCPConfigFile(): Promise<void> {
|
||||
try {
|
||||
const mcpConfigUri = await this._getMCPConfigFilePath();
|
||||
await this.editorService.openEditor({
|
||||
resource: mcpConfigUri,
|
||||
options: {
|
||||
pinned: true,
|
||||
revealIfOpened: true,
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error opening MCP config file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
public getMCPTools(): InternalToolInfo[] | undefined {
|
||||
const allTools: InternalToolInfo[] = []
|
||||
for (const serverName in this.state.mcpServerOfName) {
|
||||
const server = this.state.mcpServerOfName[serverName];
|
||||
server.tools?.forEach(tool => {
|
||||
allTools.push({
|
||||
description: tool.description || '',
|
||||
params: this._transformInputSchemaToParams(tool.inputSchema),
|
||||
name: tool.name,
|
||||
mcpServerName: serverName,
|
||||
})
|
||||
})
|
||||
}
|
||||
if (allTools.length === 0) return undefined
|
||||
return allTools
|
||||
}
|
||||
|
||||
private _transformInputSchemaToParams(inputSchema?: Record<string, any>): { [paramName: string]: { description: string } } {
|
||||
|
||||
// Check if inputSchema is valid
|
||||
if (!inputSchema || !inputSchema.properties) return {};
|
||||
|
||||
const params: { [paramName: string]: { description: string } } = {};
|
||||
Object.keys(inputSchema.properties).forEach(paramName => {
|
||||
const propertyValues = inputSchema.properties[paramName];
|
||||
|
||||
// Check if propertyValues is not an object
|
||||
if (typeof propertyValues !== 'object') {
|
||||
console.warn(`Invalid property value for ${paramName}: expected object, got ${typeof propertyValues}`);
|
||||
return; // in forEach the return is equivalent to continue
|
||||
}
|
||||
|
||||
// Add the parameter to the params object
|
||||
params[paramName] = {
|
||||
description: JSON.stringify(propertyValues.description || '', null, 2) || '',
|
||||
}
|
||||
});
|
||||
return params;
|
||||
}
|
||||
|
||||
private async _getMCPConfigFilePath(): Promise<URI> {
|
||||
const appName = this.productService.dataFolderName
|
||||
const userHome = await this.pathService.userHome();
|
||||
const uri = URI.joinPath(userHome, appName, MCP_CONFIG_FILE_NAME)
|
||||
return uri
|
||||
}
|
||||
|
||||
private async _configFileExists(mcpConfigUri: URI): Promise<boolean> {
|
||||
try {
|
||||
await this.fileService.stat(mcpConfigUri);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async _parseMCPConfigFile(): Promise<MCPConfigFileJSON | null> {
|
||||
const mcpConfigUri = await this._getMCPConfigFilePath();
|
||||
try {
|
||||
const fileContent = await this.fileService.readFile(mcpConfigUri);
|
||||
const contentString = fileContent.value.toString();
|
||||
const configFileJson = JSON.parse(contentString);
|
||||
if (!configFileJson.mcpServers) {
|
||||
throw new Error('Missing mcpServers property');
|
||||
}
|
||||
return configFileJson as MCPConfigFileJSON;
|
||||
} catch (error) {
|
||||
const fullError = `Error parsing MCP config file: ${error}`;
|
||||
this._setHasError(fullError)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle server state changes
|
||||
private async _refreshMCPServers(): Promise<void> {
|
||||
|
||||
this._setHasError(undefined)
|
||||
|
||||
const newConfigFileJSON = await this._parseMCPConfigFile();
|
||||
if (!newConfigFileJSON) { console.log(`Not setting state: MCP config file not found`); return }
|
||||
if (!newConfigFileJSON?.mcpServers) { console.log(`Not setting state: MCP config file did not have an 'mcpServers' field`); return }
|
||||
|
||||
|
||||
const oldConfigFileNames = Object.keys(this.state.mcpServerOfName)
|
||||
const newConfigFileNames = Object.keys(newConfigFileJSON.mcpServers)
|
||||
|
||||
const addedServerNames = newConfigFileNames.filter(serverName => !oldConfigFileNames.includes(serverName)); // in new and not in old
|
||||
const removedServerNames = oldConfigFileNames.filter(serverName => !newConfigFileNames.includes(serverName)); // in old and not in new
|
||||
|
||||
// set isOn to any new servers in the config
|
||||
const addedUserStateOfName: MCPUserStateOfName = {}
|
||||
for (const name of addedServerNames) { addedUserStateOfName[name] = { isOn: true } }
|
||||
await this.voidSettingsService.addMCPUserStateOfNames(addedUserStateOfName);
|
||||
|
||||
// delete isOn for any servers that no longer show up in the config
|
||||
await this.voidSettingsService.removeMCPUserStateOfNames(removedServerNames);
|
||||
|
||||
// set all servers to loading
|
||||
for (const serverName in newConfigFileJSON.mcpServers) {
|
||||
this._setMCPServerState(serverName, { status: 'loading', tools: [] })
|
||||
}
|
||||
const updatedServerNames = Object.keys(newConfigFileJSON.mcpServers).filter(serverName => !addedServerNames.includes(serverName) && !removedServerNames.includes(serverName))
|
||||
|
||||
this.channel.call('refreshMCPServers', {
|
||||
mcpConfigFileJSON: newConfigFileJSON,
|
||||
addedServerNames,
|
||||
removedServerNames,
|
||||
updatedServerNames,
|
||||
userStateOfName: this.voidSettingsService.state.mcpUserStateOfName,
|
||||
})
|
||||
}
|
||||
|
||||
stringifyResult(result: RawMCPToolCall): string {
|
||||
let toolResultStr: string
|
||||
if (result.event === 'text') {
|
||||
toolResultStr = result.text
|
||||
} else if (result.event === 'image') {
|
||||
toolResultStr = `[Image: ${result.image.mimeType}]`
|
||||
} else if (result.event === 'audio') {
|
||||
toolResultStr = `[Audio content]`
|
||||
} else if (result.event === 'resource') {
|
||||
toolResultStr = `[Resource content]`
|
||||
} else {
|
||||
toolResultStr = JSON.stringify(result)
|
||||
}
|
||||
return toolResultStr
|
||||
}
|
||||
|
||||
// toggle MCP server and update isOn in void settings
|
||||
public async toggleServerIsOn(serverName: string, isOn: boolean): Promise<void> {
|
||||
this._setMCPServerState(serverName, { status: 'loading', tools: [] })
|
||||
|
||||
await this.voidSettingsService.setMCPServerState(serverName, { isOn });
|
||||
this.channel.call('toggleMCPServer', { serverName, isOn })
|
||||
}
|
||||
|
||||
|
||||
public async callMCPTool(toolData: MCPToolCallParams): Promise<{ result: RawMCPToolCall }> {
|
||||
const result = await this.channel.call<RawMCPToolCall>('callTool', toolData);
|
||||
if (result.event === 'error') {
|
||||
throw new Error(`Error: ${result.text}`)
|
||||
}
|
||||
return { result };
|
||||
}
|
||||
|
||||
// public getMCPToolFns(): MCPToolResultType {
|
||||
// const tools = this.getMCPTools();
|
||||
// const toolFns: MCPToolResultType = {};
|
||||
|
||||
// tools.forEach((tool) => {
|
||||
// const name = tool.name;
|
||||
// // Define the tool call function
|
||||
// const toolFn = async (params: {
|
||||
// serverName: string,
|
||||
// toolName: string,
|
||||
// args: any
|
||||
// }) => {
|
||||
// const { serverName, toolName, args } = params;
|
||||
// const response = await this.callMCPTool({
|
||||
// serverName,
|
||||
// toolName,
|
||||
// params: args,
|
||||
// });
|
||||
// return { result: response }
|
||||
// };
|
||||
// toolFns[name] = toolFn;
|
||||
// });
|
||||
|
||||
// return toolFns
|
||||
// }
|
||||
}
|
||||
|
||||
registerSingleton(IMCPService, MCPService, InstantiationType.Eager);
|
||||
236
src/vs/workbench/contrib/void/common/mcpServiceTypes.ts
Normal file
236
src/vs/workbench/contrib/void/common/mcpServiceTypes.ts
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
/**
|
||||
* mcp-response-types.ts
|
||||
* --------------------------------------------------
|
||||
* **Pure** TypeScript interfaces (no external imports)
|
||||
* describing the JSON-RPC response shapes for:
|
||||
*
|
||||
* 1. tools/list -> ToolsListResponse
|
||||
* 2. prompts/list -> PromptsListResponse
|
||||
* 3. tools/call -> ToolCallResponse
|
||||
*
|
||||
* They are distilled directly from the official MCP
|
||||
* 2025‑03‑26 specification:
|
||||
* • Tools list response examples
|
||||
* • Prompts list response examples
|
||||
* • Tool call response examples
|
||||
*
|
||||
* Use them to get full IntelliSense when working with
|
||||
* @modelcontextprotocol/inspector‑cli responses.
|
||||
*/
|
||||
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Core JSON‑RPC envelope */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
// export interface JsonRpcSuccess<T> {
|
||||
// /** JSON‑RPC version – always '2.0' */
|
||||
// jsonrpc: '2.0';
|
||||
// /** Request identifier echoed back by the server */
|
||||
// id: string | number | null;
|
||||
// /** The successful result payload */
|
||||
// result: T;
|
||||
// }
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Utility: pagination */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
// export interface Paginated {
|
||||
// /** Opaque cursor for fetching the next page */
|
||||
// nextCursor?: string;
|
||||
// }
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* 1. tools/list */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
export interface MCPTool {
|
||||
/** Unique tool identifier */
|
||||
name: string;
|
||||
/** Human‑readable description */
|
||||
description?: string;
|
||||
/** JSON schema describing expected arguments */
|
||||
inputSchema?: Record<string, unknown>;
|
||||
/** Free‑form annotations describing behaviour, security, etc. */
|
||||
annotations?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// export interface ToolsListResult extends Paginated {
|
||||
// tools: MCPTool[];
|
||||
// }
|
||||
|
||||
// export type ToolsListResponse = JsonRpcSuccess<ToolsListResult>;
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* 2. prompts/list */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
// export interface PromptArgument {
|
||||
// name: string;
|
||||
// description?: string;
|
||||
// /** Whether the argument is required */
|
||||
// required?: boolean;
|
||||
// }
|
||||
|
||||
// export interface Prompt {
|
||||
// name: string;
|
||||
// description?: string;
|
||||
// arguments?: PromptArgument[];
|
||||
// }
|
||||
|
||||
// export interface PromptsListResult extends Paginated {
|
||||
// prompts: Prompt[];
|
||||
// }
|
||||
|
||||
// export type PromptsListResponse = JsonRpcSuccess<PromptsListResult>;
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* 3. tools/call */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
/** Additional resource structure that can be embedded in tool results */
|
||||
// export interface Resource {
|
||||
// uri: string;
|
||||
// mimeType: string;
|
||||
// /** Either plain‑text or base64‑encoded binary data */
|
||||
// text?: string;
|
||||
// data?: string;
|
||||
// }
|
||||
|
||||
/** Individual content items returned by a tool */
|
||||
// export type ToolContent =
|
||||
// | { type: 'text'; text: string }
|
||||
// | { type: 'image'; data: string; mimeType: string }
|
||||
// | { type: 'audio'; data: string; mimeType: string }
|
||||
// | { type: 'resource'; resource: Resource };
|
||||
|
||||
// export interface ToolCallResult {
|
||||
// /** List of content parts (text, images, resources, etc.) */
|
||||
// content: ToolContent[];
|
||||
// /** True if the tool itself encountered a domain‑level error */
|
||||
// isError?: boolean;
|
||||
// }
|
||||
|
||||
// export type ToolCallResponse = JsonRpcSuccess<ToolCallResult>;
|
||||
|
||||
// MCP SERVER CONFIG FILE TYPES -----------------------------
|
||||
|
||||
export interface MCPConfigFileEntryJSON {
|
||||
// Command-based server properties
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
|
||||
// URL-based server properties
|
||||
url?: URL;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface MCPConfigFileJSON {
|
||||
mcpServers: Record<string, MCPConfigFileEntryJSON>;
|
||||
}
|
||||
|
||||
|
||||
// SERVER EVENT TYPES ------------------------------------------
|
||||
|
||||
export type MCPServer = {
|
||||
// Command-based server properties
|
||||
tools: MCPTool[],
|
||||
status: 'loading' | 'success' | 'offline',
|
||||
command?: string,
|
||||
error?: string,
|
||||
} | {
|
||||
tools?: undefined,
|
||||
status: 'error',
|
||||
command?: string,
|
||||
error: string,
|
||||
}
|
||||
|
||||
export interface MCPServerOfName {
|
||||
[serverName: string]: MCPServer;
|
||||
}
|
||||
|
||||
export type MCPServerEvent = {
|
||||
name: string;
|
||||
prevServer?: MCPServer;
|
||||
newServer?: MCPServer;
|
||||
}
|
||||
export type MCPServerEventResponse = { response: MCPServerEvent }
|
||||
|
||||
export interface MCPConfigFileParseErrorResponse {
|
||||
response: {
|
||||
type: 'config-file-error';
|
||||
error: string | null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// export type MCPServerResponse = MCPAddResponse | MCPUpdateResponse | MCPDeleteResponse | MCPLoadingResponse;
|
||||
|
||||
// Event parameter types
|
||||
// export type MCPServerEventAddParam = { response: MCPAddResponse };
|
||||
// export type MCPServerEventUpdateParam = { response: MCPUpdateResponse };
|
||||
// export type MCPServerEventDeleteParam = { response: MCPDeleteResponse };
|
||||
// export type MCPServerEventLoadingParam = { response: MCPLoadingResponse };
|
||||
|
||||
// Event Param union type
|
||||
// export type MCPServerEventParam = MCPServerEventAddParam | MCPServerEventUpdateParam | MCPServerEventDeleteParam | MCPServerEventLoadingParam;
|
||||
|
||||
// TOOL CALL EVENT TYPES ------------------------------------------
|
||||
|
||||
type MCPToolResponseType = 'text' | 'image' | 'audio' | 'resource' | 'error';
|
||||
|
||||
type ResponseImageTypes = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp' | 'image/svg+xml' | 'image/bmp' | 'image/tiff' | 'image/vnd.microsoft.icon';
|
||||
|
||||
interface ImageData {
|
||||
data: string;
|
||||
mimeType: ResponseImageTypes;
|
||||
}
|
||||
|
||||
interface MCPToolResponseBase {
|
||||
toolName: string;
|
||||
serverName?: string;
|
||||
event: MCPToolResponseType;
|
||||
text?: string;
|
||||
image?: ImageData;
|
||||
}
|
||||
|
||||
type MCPToolResponseConstraints = {
|
||||
'text': {
|
||||
image?: never;
|
||||
text: string;
|
||||
};
|
||||
'error': {
|
||||
image?: never;
|
||||
text: string;
|
||||
};
|
||||
'image': {
|
||||
text?: never;
|
||||
image: ImageData;
|
||||
};
|
||||
'audio': {
|
||||
text?: never;
|
||||
image?: never;
|
||||
};
|
||||
'resource': {
|
||||
text?: never;
|
||||
image?: never;
|
||||
}
|
||||
}
|
||||
|
||||
type MCPToolEventResponse<T extends MCPToolResponseType> = Omit<MCPToolResponseBase, 'event' | keyof MCPToolResponseConstraints> & MCPToolResponseConstraints[T] & { event: T };
|
||||
|
||||
// Response types
|
||||
export type MCPToolTextResponse = MCPToolEventResponse<'text'>;
|
||||
export type MCPToolErrorResponse = MCPToolEventResponse<'error'>;
|
||||
export type MCPToolImageResponse = MCPToolEventResponse<'image'>;
|
||||
export type MCPToolAudioResponse = MCPToolEventResponse<'audio'>;
|
||||
export type MCPToolResourceResponse = MCPToolEventResponse<'resource'>;
|
||||
export type RawMCPToolCall = MCPToolTextResponse | MCPToolErrorResponse | MCPToolImageResponse | MCPToolAudioResponse | MCPToolResourceResponse;
|
||||
|
||||
export interface MCPToolCallParams {
|
||||
serverName: string;
|
||||
toolName: string;
|
||||
params: Record<string, unknown>;
|
||||
}
|
||||
|
|
@ -1375,7 +1375,6 @@ const openRouterModelOptions_assumingOpenAICompat = {
|
|||
|
||||
const openRouterSettings: VoidStaticProviderInfo = {
|
||||
modelOptions: openRouterModelOptions_assumingOpenAICompat,
|
||||
// TODO!!! send a query to openrouter to get the price, etc.
|
||||
modelOptionsFallback: (modelName) => {
|
||||
const res = extensiveModelOptionsFallback(modelName)
|
||||
// openRouter does not support gemini-style, use openai-style instead
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { IDirectoryStrService } from '../directoryStrService.js';
|
|||
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
|
||||
import { os } from '../helpers/systemInfo.js';
|
||||
import { RawToolParamsObj } from '../sendLLMMessageTypes.js';
|
||||
import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../toolsServiceTypes.js';
|
||||
import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, BuiltinToolName, BuiltinToolResultType, ToolName } from '../toolsServiceTypes.js';
|
||||
import { ChatMode } from '../voidSettingsTypes.js';
|
||||
|
||||
// Triple backtick wrapper used throughout the prompts for code blocks
|
||||
|
|
@ -147,6 +147,8 @@ export type InternalToolInfo = {
|
|||
params: {
|
||||
[paramName: string]: { description: string }
|
||||
},
|
||||
// Only if the tool is from an MCP server
|
||||
mcpServerName?: string,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -181,191 +183,197 @@ export type SnakeCaseKeys<T extends Record<string, any>> = {
|
|||
|
||||
|
||||
|
||||
// export const voidTools = {
|
||||
export const voidTools
|
||||
: {
|
||||
[T in keyof ToolCallParams]: {
|
||||
name: string;
|
||||
description: string;
|
||||
// more params can be generated than exist here, but these params must be a subset of them
|
||||
params: Partial<{ [paramName in keyof SnakeCaseKeys<ToolCallParams[T]>]: { description: string } }>
|
||||
}
|
||||
export const builtinTools: {
|
||||
[T in keyof BuiltinToolCallParams]: {
|
||||
name: string;
|
||||
description: string;
|
||||
// more params can be generated than exist here, but these params must be a subset of them
|
||||
params: Partial<{ [paramName in keyof SnakeCaseKeys<BuiltinToolCallParams[T]>]: { description: string } }>
|
||||
}
|
||||
= {
|
||||
// --- context-gathering (read/search/list) ---
|
||||
} = {
|
||||
// --- context-gathering (read/search/list) ---
|
||||
|
||||
read_file: {
|
||||
name: 'read_file',
|
||||
description: `Returns full contents of a given file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
start_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the beginning of the file.' },
|
||||
end_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the end of the file.' },
|
||||
...paginationParam,
|
||||
},
|
||||
read_file: {
|
||||
name: 'read_file',
|
||||
description: `Returns full contents of a given file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
start_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the beginning of the file.' },
|
||||
end_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the end of the file.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
ls_dir: {
|
||||
name: 'ls_dir',
|
||||
description: `Lists all files and folders in the given URI.`,
|
||||
params: {
|
||||
uri: { description: `Optional. The FULL path to the ${'folder'}. Leave this as empty or "" to search all folders.` },
|
||||
...paginationParam,
|
||||
},
|
||||
ls_dir: {
|
||||
name: 'ls_dir',
|
||||
description: `Lists all files and folders in the given URI.`,
|
||||
params: {
|
||||
uri: { description: `Optional. The FULL path to the ${'folder'}. Leave this as empty or "" to search all folders.` },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
get_dir_tree: {
|
||||
name: 'get_dir_tree',
|
||||
description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `,
|
||||
params: {
|
||||
...uriParam('folder')
|
||||
}
|
||||
},
|
||||
|
||||
// pathname_search: {
|
||||
// name: 'pathname_search',
|
||||
// description: `Returns all pathnames that match a given \`find\`-style query over the entire workspace. ONLY searches file names. ONLY searches the current workspace. You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
|
||||
|
||||
search_pathnames_only: {
|
||||
name: 'search_pathnames_only',
|
||||
description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`,
|
||||
params: {
|
||||
query: { description: `Your query for the search.` },
|
||||
include_pattern: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
|
||||
search_for_files: {
|
||||
name: 'search_for_files',
|
||||
description: `Returns a list of file names whose content matches the given query. The query can be any substring or regex.`,
|
||||
params: {
|
||||
query: { description: `Your query for the search.` },
|
||||
search_in_folder: { description: 'Optional. Leave as blank by default. ONLY fill this in if your previous search with the same query was truncated. Searches descendants of this folder only.' },
|
||||
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
// add new search_in_file tool
|
||||
search_in_file: {
|
||||
name: 'search_in_file',
|
||||
description: `Returns an array of all the start line numbers where the content appears in the file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
query: { description: 'The string or regex to search for in the file.' },
|
||||
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' }
|
||||
}
|
||||
},
|
||||
|
||||
read_lint_errors: {
|
||||
name: 'read_lint_errors',
|
||||
description: `Use this tool to view all the lint errors on a file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
},
|
||||
},
|
||||
|
||||
// --- editing (create/delete) ---
|
||||
|
||||
create_file_or_folder: {
|
||||
name: 'create_file_or_folder',
|
||||
description: `Create a file or folder at the given path. To create a folder, the path MUST end with a trailing slash.`,
|
||||
params: {
|
||||
...uriParam('file or folder'),
|
||||
},
|
||||
},
|
||||
|
||||
delete_file_or_folder: {
|
||||
name: 'delete_file_or_folder',
|
||||
description: `Delete a file or folder at the given path.`,
|
||||
params: {
|
||||
...uriParam('file or folder'),
|
||||
is_recursive: { description: 'Optional. Return true to delete recursively.' }
|
||||
},
|
||||
},
|
||||
|
||||
edit_file: {
|
||||
name: 'edit_file',
|
||||
description: `Edit the contents of a file. You must provide the file's URI as well as a SINGLE string of SEARCH/REPLACE block(s) that will be used to apply the edit.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
search_replace_blocks: { description: replaceTool_description }
|
||||
},
|
||||
},
|
||||
|
||||
rewrite_file: {
|
||||
name: 'rewrite_file',
|
||||
description: `Edits a file, deleting all the old contents and replacing them with your new contents. Use this tool if you want to edit a file you just created.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
new_content: { description: `The new contents of the file. Must be a string.` }
|
||||
},
|
||||
},
|
||||
run_command: {
|
||||
name: 'run_command',
|
||||
description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). ${terminalDescHelper}`,
|
||||
params: {
|
||||
command: { description: 'The terminal command to run.' },
|
||||
cwd: { description: cwdHelper },
|
||||
},
|
||||
},
|
||||
|
||||
run_persistent_command: {
|
||||
name: 'run_persistent_command',
|
||||
description: `Runs a terminal command in the persistent terminal that you created with open_persistent_terminal (results after ${MAX_TERMINAL_BG_COMMAND_TIME} are returned, and command continues running in background). ${terminalDescHelper}`,
|
||||
params: {
|
||||
command: { description: 'The terminal command to run.' },
|
||||
persistent_terminal_id: { description: 'The ID of the terminal created using open_persistent_terminal.' },
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
|
||||
open_persistent_terminal: {
|
||||
name: 'open_persistent_terminal',
|
||||
description: `Use this tool when you want to run a terminal command indefinitely, like a dev server (eg \`npm run dev\`), a background listener, etc. Opens a new terminal in the user's environment which will not awaited for or killed.`,
|
||||
params: {
|
||||
cwd: { description: cwdHelper },
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
kill_persistent_terminal: {
|
||||
name: 'kill_persistent_terminal',
|
||||
description: `Interrupts and closes a persistent terminal that you opened with open_persistent_terminal.`,
|
||||
params: { persistent_terminal_id: { description: `The ID of the persistent terminal.` } }
|
||||
get_dir_tree: {
|
||||
name: 'get_dir_tree',
|
||||
description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `,
|
||||
params: {
|
||||
...uriParam('folder')
|
||||
}
|
||||
},
|
||||
|
||||
// pathname_search: {
|
||||
// name: 'pathname_search',
|
||||
// description: `Returns all pathnames that match a given \`find\`-style query over the entire workspace. ONLY searches file names. ONLY searches the current workspace. You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
|
||||
|
||||
search_pathnames_only: {
|
||||
name: 'search_pathnames_only',
|
||||
description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`,
|
||||
params: {
|
||||
query: { description: `Your query for the search.` },
|
||||
include_pattern: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
// go_to_definition
|
||||
// go_to_usages
|
||||
|
||||
} satisfies { [T in keyof ToolResultType]: InternalToolInfo }
|
||||
search_for_files: {
|
||||
name: 'search_for_files',
|
||||
description: `Returns a list of file names whose content matches the given query. The query can be any substring or regex.`,
|
||||
params: {
|
||||
query: { description: `Your query for the search.` },
|
||||
search_in_folder: { description: 'Optional. Leave as blank by default. ONLY fill this in if your previous search with the same query was truncated. Searches descendants of this folder only.' },
|
||||
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' },
|
||||
...paginationParam,
|
||||
},
|
||||
},
|
||||
|
||||
// add new search_in_file tool
|
||||
search_in_file: {
|
||||
name: 'search_in_file',
|
||||
description: `Returns an array of all the start line numbers where the content appears in the file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
query: { description: 'The string or regex to search for in the file.' },
|
||||
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' }
|
||||
}
|
||||
},
|
||||
|
||||
read_lint_errors: {
|
||||
name: 'read_lint_errors',
|
||||
description: `Use this tool to view all the lint errors on a file.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
},
|
||||
},
|
||||
|
||||
// --- editing (create/delete) ---
|
||||
|
||||
create_file_or_folder: {
|
||||
name: 'create_file_or_folder',
|
||||
description: `Create a file or folder at the given path. To create a folder, the path MUST end with a trailing slash.`,
|
||||
params: {
|
||||
...uriParam('file or folder'),
|
||||
},
|
||||
},
|
||||
|
||||
delete_file_or_folder: {
|
||||
name: 'delete_file_or_folder',
|
||||
description: `Delete a file or folder at the given path.`,
|
||||
params: {
|
||||
...uriParam('file or folder'),
|
||||
is_recursive: { description: 'Optional. Return true to delete recursively.' }
|
||||
},
|
||||
},
|
||||
|
||||
edit_file: {
|
||||
name: 'edit_file',
|
||||
description: `Edit the contents of a file. You must provide the file's URI as well as a SINGLE string of SEARCH/REPLACE block(s) that will be used to apply the edit.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
search_replace_blocks: { description: replaceTool_description }
|
||||
},
|
||||
},
|
||||
|
||||
rewrite_file: {
|
||||
name: 'rewrite_file',
|
||||
description: `Edits a file, deleting all the old contents and replacing them with your new contents. Use this tool if you want to edit a file you just created.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
new_content: { description: `The new contents of the file. Must be a string.` }
|
||||
},
|
||||
},
|
||||
run_command: {
|
||||
name: 'run_command',
|
||||
description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). ${terminalDescHelper}`,
|
||||
params: {
|
||||
command: { description: 'The terminal command to run.' },
|
||||
cwd: { description: cwdHelper },
|
||||
},
|
||||
},
|
||||
|
||||
run_persistent_command: {
|
||||
name: 'run_persistent_command',
|
||||
description: `Runs a terminal command in the persistent terminal that you created with open_persistent_terminal (results after ${MAX_TERMINAL_BG_COMMAND_TIME} are returned, and command continues running in background). ${terminalDescHelper}`,
|
||||
params: {
|
||||
command: { description: 'The terminal command to run.' },
|
||||
persistent_terminal_id: { description: 'The ID of the terminal created using open_persistent_terminal.' },
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
export type ToolName = keyof ToolResultType
|
||||
export const toolNames = Object.keys(voidTools) as ToolName[]
|
||||
|
||||
type ToolParamNameOfTool<T extends ToolName> = keyof (typeof voidTools)[T]['params']
|
||||
export type ToolParamName = { [T in ToolName]: ToolParamNameOfTool<T> }[ToolName]
|
||||
open_persistent_terminal: {
|
||||
name: 'open_persistent_terminal',
|
||||
description: `Use this tool when you want to run a terminal command indefinitely, like a dev server (eg \`npm run dev\`), a background listener, etc. Opens a new terminal in the user's environment which will not awaited for or killed.`,
|
||||
params: {
|
||||
cwd: { description: cwdHelper },
|
||||
}
|
||||
},
|
||||
|
||||
const toolNamesSet = new Set<string>(toolNames)
|
||||
|
||||
export const isAToolName = (toolName: string): toolName is ToolName => {
|
||||
kill_persistent_terminal: {
|
||||
name: 'kill_persistent_terminal',
|
||||
description: `Interrupts and closes a persistent terminal that you opened with open_persistent_terminal.`,
|
||||
params: { persistent_terminal_id: { description: `The ID of the persistent terminal.` } }
|
||||
}
|
||||
|
||||
|
||||
// go_to_definition
|
||||
// go_to_usages
|
||||
|
||||
} satisfies { [T in keyof BuiltinToolResultType]: InternalToolInfo }
|
||||
|
||||
|
||||
|
||||
|
||||
export const builtinToolNames = Object.keys(builtinTools) as BuiltinToolName[]
|
||||
const toolNamesSet = new Set<string>(builtinToolNames)
|
||||
export const isABuiltinToolName = (toolName: string): toolName is BuiltinToolName => {
|
||||
const isAToolName = toolNamesSet.has(toolName)
|
||||
return isAToolName
|
||||
}
|
||||
|
||||
export const availableTools = (chatMode: ChatMode) => {
|
||||
const toolNames: ToolName[] | undefined = chatMode === 'normal' ? undefined
|
||||
: chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !(toolName in approvalTypeOfToolName))
|
||||
: chatMode === 'agent' ? Object.keys(voidTools) as ToolName[]
|
||||
|
||||
|
||||
|
||||
|
||||
export const availableTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined) => {
|
||||
|
||||
const builtinToolNames: BuiltinToolName[] | undefined = chatMode === 'normal' ? undefined
|
||||
: chatMode === 'gather' ? (Object.keys(builtinTools) as BuiltinToolName[]).filter(toolName => !(toolName in approvalTypeOfBuiltinToolName))
|
||||
: chatMode === 'agent' ? Object.keys(builtinTools) as BuiltinToolName[]
|
||||
: undefined
|
||||
|
||||
const tools: InternalToolInfo[] | undefined = toolNames?.map(toolName => voidTools[toolName])
|
||||
const effectiveBuiltinTools = builtinToolNames?.map(toolName => builtinTools[toolName]) ?? undefined
|
||||
const effectiveMCPTools = chatMode === 'agent' ? mcpTools : undefined
|
||||
|
||||
const tools: InternalToolInfo[] | undefined = !(builtinToolNames || mcpTools) ? undefined
|
||||
: [
|
||||
...effectiveBuiltinTools ?? [],
|
||||
...effectiveMCPTools ?? [],
|
||||
]
|
||||
|
||||
return tools
|
||||
}
|
||||
|
||||
|
|
@ -382,7 +390,7 @@ const toolCallDefinitionsXMLString = (tools: InternalToolInfo[]) => {
|
|||
}
|
||||
|
||||
export const reParsedToolXMLString = (toolName: ToolName, toolParams: RawToolParamsObj) => {
|
||||
const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName as ToolParamName]}</${paramName}>`).join('\n')
|
||||
const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName]}</${paramName}>`).join('\n')
|
||||
return `\
|
||||
<${toolName}>${!params ? '' : `\n${params}`}
|
||||
</${toolName}>`
|
||||
|
|
@ -391,8 +399,8 @@ export const reParsedToolXMLString = (toolName: ToolName, toolParams: RawToolPar
|
|||
|
||||
/* We expect tools to come at the end - not a hard limit, but that's just how we process them, and the flow makes more sense that way. */
|
||||
// - You are allowed to call multiple tools by specifying them consecutively. However, there should be NO text or writing between tool calls or after them.
|
||||
const systemToolsXMLPrompt = (chatMode: ChatMode) => {
|
||||
const tools = availableTools(chatMode)
|
||||
const systemToolsXMLPrompt = (chatMode: ChatMode, mcpTools: InternalToolInfo[] | undefined) => {
|
||||
const tools = availableTools(chatMode, mcpTools)
|
||||
if (!tools || tools.length === 0) return null
|
||||
|
||||
const toolXMLDefinitions = (`\
|
||||
|
|
@ -417,7 +425,7 @@ const systemToolsXMLPrompt = (chatMode: ChatMode) => {
|
|||
// ======================================================== chat (normal, gather, agent) ========================================================
|
||||
|
||||
|
||||
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, persistentTerminalIDs, directoryStr, chatMode: mode, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, persistentTerminalIDs: string[], chatMode: ChatMode, includeXMLToolDefinitions: boolean }) => {
|
||||
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, persistentTerminalIDs, directoryStr, chatMode: mode, mcpTools, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, persistentTerminalIDs: string[], chatMode: ChatMode, mcpTools: InternalToolInfo[] | undefined, includeXMLToolDefinitions: boolean }) => {
|
||||
const header = (`You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} whose job is \
|
||||
${mode === 'agent' ? `to help the user develop, run, and make changes to their codebase.`
|
||||
: mode === 'gather' ? `to search, understand, and reference files in the user's codebase.`
|
||||
|
|
@ -451,7 +459,7 @@ ${directoryStr}
|
|||
</files_overview>`)
|
||||
|
||||
|
||||
const toolDefinitions = includeXMLToolDefinitions ? systemToolsXMLPrompt(mode) : null
|
||||
const toolDefinitions = includeXMLToolDefinitions ? systemToolsXMLPrompt(mode, mcpTools) : null
|
||||
|
||||
const details: string[] = []
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { generateUuid } from '../../../../base/common/uuid.js';
|
|||
import { Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IVoidSettingsService } from './voidSettingsService.js';
|
||||
import { IMCPService } from './mcpService.js';
|
||||
|
||||
// calls channel to implement features
|
||||
export const ILLMMessageService = createDecorator<ILLMMessageService>('llmMessageService');
|
||||
|
|
@ -61,6 +62,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
|||
@IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side)
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
// @INotificationService private readonly notificationService: INotificationService,
|
||||
@IMCPService private readonly mcpService: IMCPService,
|
||||
) {
|
||||
super()
|
||||
|
||||
|
|
@ -116,6 +118,8 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
|||
|
||||
const { settingsOfProvider, } = this.voidSettingsService.state
|
||||
|
||||
const mcpTools = this.mcpService.getMCPTools()
|
||||
|
||||
// add state for request id
|
||||
const requestId = generateUuid();
|
||||
this.llmMessageHooks.onText[requestId] = onText
|
||||
|
|
@ -129,6 +133,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
|||
requestId,
|
||||
settingsOfProvider,
|
||||
modelSelection,
|
||||
mcpTools,
|
||||
} satisfies MainSendLLMMessageParams);
|
||||
|
||||
return requestId
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ToolName, ToolParamName } from './prompt/prompts.js'
|
||||
import { InternalToolInfo } from './prompt/prompts.js'
|
||||
import { ToolName, ToolParamName } from './toolsServiceTypes.js'
|
||||
import { ChatMode, ModelSelection, ModelSelectionOptions, OverridesOfModel, ProviderName, RefreshableProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||
|
||||
|
||||
|
|
@ -78,12 +79,12 @@ export type LLMFIMMessage = {
|
|||
|
||||
|
||||
export type RawToolParamsObj = {
|
||||
[paramName in ToolParamName]?: string;
|
||||
[paramName in ToolParamName<ToolName>]?: string;
|
||||
}
|
||||
export type RawToolCallObj = {
|
||||
name: ToolName;
|
||||
rawParams: RawToolParamsObj;
|
||||
doneParams: ToolParamName[];
|
||||
doneParams: ToolParamName<ToolName>[];
|
||||
id: string;
|
||||
isDone: boolean;
|
||||
};
|
||||
|
|
@ -133,6 +134,7 @@ export type SendLLMMessageParams = {
|
|||
overridesOfModel: OverridesOfModel | undefined;
|
||||
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
mcpTools: InternalToolInfo[] | undefined;
|
||||
} & SendLLMType
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { URI } from '../../../../base/common/uri.js'
|
||||
import { ToolName } from './prompt/prompts.js';
|
||||
import { RawMCPToolCall } from './mcpServiceTypes.js';
|
||||
import { builtinTools } from './prompt/prompts.js';
|
||||
import { RawToolParamsObj } from './sendLLMMessageTypes.js';
|
||||
|
||||
|
||||
|
||||
|
|
@ -16,7 +18,7 @@ export type ShallowDirectoryItem = {
|
|||
}
|
||||
|
||||
|
||||
export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'terminal' }> = {
|
||||
export const approvalTypeOfBuiltinToolName: Partial<{ [T in BuiltinToolName]?: 'edits' | 'terminal' | 'mcp-tools' }> = {
|
||||
'create_file_or_folder': 'edits',
|
||||
'delete_file_or_folder': 'edits',
|
||||
'rewrite_file': 'edits',
|
||||
|
|
@ -28,16 +30,19 @@ export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'term
|
|||
}
|
||||
|
||||
|
||||
export type ToolApprovalType = NonNullable<(typeof approvalTypeOfBuiltinToolName)[keyof typeof approvalTypeOfBuiltinToolName]>;
|
||||
|
||||
|
||||
export const toolApprovalTypes = new Set<ToolApprovalType>([
|
||||
...Object.values(approvalTypeOfBuiltinToolName),
|
||||
'mcp-tools',
|
||||
])
|
||||
|
||||
|
||||
// {{add: define new type for approval types}}
|
||||
export type ToolApprovalType = NonNullable<(typeof approvalTypeOfToolName)[keyof typeof approvalTypeOfToolName]>;
|
||||
|
||||
export const toolApprovalTypes = new Set<ToolApprovalType>(
|
||||
Object.values(approvalTypeOfToolName).filter((v): v is ToolApprovalType => v !== undefined)
|
||||
)
|
||||
|
||||
// PARAMS OF TOOL CALL
|
||||
export type ToolCallParams = {
|
||||
export type BuiltinToolCallParams = {
|
||||
'read_file': { uri: URI, startLine: number | null, endLine: number | null, pageNumber: number },
|
||||
'ls_dir': { uri: URI, pageNumber: number },
|
||||
'get_dir_tree': { uri: URI },
|
||||
|
|
@ -58,7 +63,7 @@ export type ToolCallParams = {
|
|||
}
|
||||
|
||||
// RESULT OF TOOL CALL
|
||||
export type ToolResultType = {
|
||||
export type BuiltinToolResultType = {
|
||||
'read_file': { fileContents: string, totalFileLen: number, totalNumLines: number, hasNextPage: boolean },
|
||||
'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
|
||||
'get_dir_tree': { str: string, },
|
||||
|
|
@ -78,3 +83,15 @@ export type ToolResultType = {
|
|||
'kill_persistent_terminal': {},
|
||||
}
|
||||
|
||||
|
||||
export type ToolCallParams<T extends BuiltinToolName | (string & {})> = T extends BuiltinToolName ? BuiltinToolCallParams[T] : RawToolParamsObj
|
||||
export type ToolResult<T extends BuiltinToolName | (string & {})> = T extends BuiltinToolName ? BuiltinToolResultType[T] : RawMCPToolCall
|
||||
|
||||
export type BuiltinToolName = keyof BuiltinToolResultType
|
||||
|
||||
type BuiltinToolParamNameOfTool<T extends BuiltinToolName> = keyof (typeof builtinTools)[T]['params']
|
||||
export type BuiltinToolParamName = { [T in BuiltinToolName]: BuiltinToolParamNameOfTool<T> }[BuiltinToolName]
|
||||
|
||||
|
||||
export type ToolName = BuiltinToolName | (string & {})
|
||||
export type ToolParamName<T extends ToolName> = T extends BuiltinToolName ? BuiltinToolParamNameOfTool<T> : string
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
|
|||
import { IMetricsService } from './metricsService.js';
|
||||
import { defaultProviderSettings, getModelCapabilities, ModelOverrides } from './modelCapabilities.js';
|
||||
import { VOID_SETTINGS_STORAGE_KEY } from './storageKeys.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode, OverridesOfModel, defaultOverridesOfModel } from './voidSettingsTypes.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode, OverridesOfModel, defaultOverridesOfModel, MCPUserStateOfName as MCPUserStateOfName, MCPUserState } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
// name is the name in the dropdown
|
||||
|
|
@ -43,6 +43,7 @@ export type VoidSettingsState = {
|
|||
readonly optionsOfModelSelection: OptionsOfModelSelection;
|
||||
readonly overridesOfModel: OverridesOfModel;
|
||||
readonly globalSettings: GlobalSettings;
|
||||
readonly mcpUserStateOfName: MCPUserStateOfName; // user-controlled state of MCP servers
|
||||
|
||||
readonly _modelOptions: ModelOption[] // computed based on the two above items
|
||||
}
|
||||
|
|
@ -62,6 +63,7 @@ export interface IVoidSettingsService {
|
|||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
|
||||
setOptionsOfModelSelection: SetOptionsOfModelSelection;
|
||||
setGlobalSetting: SetGlobalSettingFn;
|
||||
// setMCPServerStates: (newStates: MCPServerStates) => Promise<void>;
|
||||
|
||||
// setting to undefined CLEARS it, unlike others:
|
||||
setOverridesOfModel(providerName: ProviderName, modelName: string, overrides: Partial<ModelOverrides> | undefined): Promise<void>;
|
||||
|
|
@ -73,6 +75,10 @@ export interface IVoidSettingsService {
|
|||
toggleModelHidden(providerName: ProviderName, modelName: string): void;
|
||||
addModel(providerName: ProviderName, modelName: string): void;
|
||||
deleteModel(providerName: ProviderName, modelName: string): boolean;
|
||||
|
||||
addMCPUserStateOfNames(userStateOfName: MCPUserStateOfName): Promise<void>;
|
||||
removeMCPUserStateOfNames(serverNames: string[]): Promise<void>;
|
||||
setMCPServerState(serverName: string, state: MCPUserState): Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -212,6 +218,7 @@ const defaultState = () => {
|
|||
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {} },
|
||||
overridesOfModel: deepClone(defaultOverridesOfModel),
|
||||
_modelOptions: [], // computed later
|
||||
mcpUserStateOfName: {},
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
|
@ -361,6 +368,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
|
||||
const newGlobalSettings = this.state.globalSettings
|
||||
const newOverridesOfModel = this.state.overridesOfModel
|
||||
const newMCPUserStateOfName = this.state.mcpUserStateOfName
|
||||
|
||||
const newState = {
|
||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
|
|
@ -368,6 +376,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
settingsOfProvider: newSettingsOfProvider,
|
||||
globalSettings: newGlobalSettings,
|
||||
overridesOfModel: newOverridesOfModel,
|
||||
mcpUserStateOfName: newMCPUserStateOfName,
|
||||
}
|
||||
|
||||
this.state = _validatedModelState(newState)
|
||||
|
|
@ -531,6 +540,55 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
return true
|
||||
}
|
||||
|
||||
// MCP Server State
|
||||
private _setMCPUserStateOfName = async (newStates: MCPUserStateOfName) => {
|
||||
const newState: VoidSettingsState = {
|
||||
...this.state,
|
||||
mcpUserStateOfName: {
|
||||
...this.state.mcpUserStateOfName,
|
||||
...newStates
|
||||
}
|
||||
};
|
||||
this.state = _validatedModelState(newState);
|
||||
await this._storeState();
|
||||
this._onDidChangeState.fire();
|
||||
this._metricsService.capture('Set MCP Server States', { newStates });
|
||||
}
|
||||
|
||||
addMCPUserStateOfNames = async (newMCPStates: MCPUserStateOfName) => {
|
||||
const { mcpUserStateOfName: mcpServerStates } = this.state
|
||||
const newMCPServerStates = {
|
||||
...mcpServerStates,
|
||||
...newMCPStates,
|
||||
}
|
||||
await this._setMCPUserStateOfName(newMCPServerStates)
|
||||
this._metricsService.capture('Add MCP Servers', { servers: Object.keys(newMCPStates).join(', ') });
|
||||
}
|
||||
|
||||
removeMCPUserStateOfNames = async (serverNames: string[]) => {
|
||||
const { mcpUserStateOfName: mcpServerStates } = this.state
|
||||
const newMCPServerStates = {
|
||||
...mcpServerStates,
|
||||
}
|
||||
serverNames.forEach(serverName => {
|
||||
if (serverName in newMCPServerStates) {
|
||||
delete newMCPServerStates[serverName]
|
||||
}
|
||||
})
|
||||
await this._setMCPUserStateOfName(newMCPServerStates)
|
||||
this._metricsService.capture('Remove MCP Servers', { servers: serverNames.join(', ') });
|
||||
}
|
||||
|
||||
setMCPServerState = async (serverName: string, state: MCPUserState) => {
|
||||
const { mcpUserStateOfName } = this.state
|
||||
const newMCPServerStates = {
|
||||
...mcpUserStateOfName,
|
||||
[serverName]: state,
|
||||
}
|
||||
await this._setMCPUserStateOfName(newMCPServerStates)
|
||||
this._metricsService.capture('Update MCP Server State', { serverName, state });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export type VoidStatefulModelInfo = { // <-- STATEFUL
|
|||
modelName: string,
|
||||
type: 'default' | 'autodetected' | 'custom';
|
||||
isHidden: boolean, // whether or not the user is hiding it (switched off)
|
||||
} // TODO!!! eventually we'd want to let the user change supportsFIM, etc on the model themselves
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -491,3 +491,13 @@ export type OverridesOfModel = {
|
|||
const overridesOfModel = {} as OverridesOfModel
|
||||
for (const providerName of providerNames) { overridesOfModel[providerName] = {} }
|
||||
export const defaultOverridesOfModel = overridesOfModel
|
||||
|
||||
|
||||
|
||||
export interface MCPUserStateOfName {
|
||||
[serverName: string]: MCPUserState | undefined;
|
||||
}
|
||||
|
||||
export interface MCPUserState {
|
||||
isOn: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@
|
|||
|
||||
import { generateUuid } from '../../../../../base/common/uuid.js'
|
||||
import { endsWithAnyPrefixOf, SurroundingsRemover } from '../../common/helpers/extractCodeFromResult.js'
|
||||
import { availableTools, InternalToolInfo, ToolName, ToolParamName } from '../../common/prompt/prompts.js'
|
||||
import { availableTools, InternalToolInfo } from '../../common/prompt/prompts.js'
|
||||
import { OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js'
|
||||
import { ToolName, ToolParamName } from '../../common/toolsServiceTypes.js'
|
||||
import { ChatMode } from '../../common/voidSettingsTypes.js'
|
||||
|
||||
|
||||
|
|
@ -164,15 +165,15 @@ const findIndexOfAny = (fullText: string, matches: string[]) => {
|
|||
|
||||
|
||||
type ToolOfToolName = { [toolName: string]: InternalToolInfo | undefined }
|
||||
const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => {
|
||||
const parseXMLPrefixToToolCall = <T extends ToolName,>(toolName: T, toolId: string, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => {
|
||||
const paramsObj: RawToolParamsObj = {}
|
||||
const doneParams: ToolParamName[] = []
|
||||
const doneParams: ToolParamName<T>[] = []
|
||||
let isDone = false
|
||||
|
||||
const getAnswer = (): RawToolCallObj => {
|
||||
// trim off all whitespace at and before first \n and after last \n for each param
|
||||
for (const p in paramsObj) {
|
||||
const paramName = p as ToolParamName
|
||||
const paramName = p as ToolParamName<T>
|
||||
const orig = paramsObj[paramName]
|
||||
if (orig === undefined) continue
|
||||
paramsObj[paramName] = trimBeforeAndAfterNewLines(orig)
|
||||
|
|
@ -202,16 +203,16 @@ const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: strin
|
|||
|
||||
const pm = new SurroundingsRemover(str)
|
||||
|
||||
const allowedParams = Object.keys(toolOfToolName[toolName]?.params ?? {}) as ToolParamName[]
|
||||
const allowedParams = Object.keys(toolOfToolName[toolName]?.params ?? {}) as ToolParamName<T>[]
|
||||
if (allowedParams.length === 0) return getAnswer()
|
||||
let latestMatchedOpenParam: null | ToolParamName = null
|
||||
let latestMatchedOpenParam: null | ToolParamName<T> = null
|
||||
let n = 0
|
||||
while (true) {
|
||||
n += 1
|
||||
if (n > 10) return getAnswer() // just for good measure as this code is early
|
||||
|
||||
// find the param name opening tag
|
||||
let matchedOpenParam: null | ToolParamName = null
|
||||
let matchedOpenParam: null | ToolParamName<T> = null
|
||||
for (const paramName of allowedParams) {
|
||||
const removed = pm.removeFromStartUntilFullMatch(`<${paramName}>`, true)
|
||||
if (removed) {
|
||||
|
|
@ -260,11 +261,14 @@ const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: strin
|
|||
}
|
||||
|
||||
export const extractXMLToolsWrapper = (
|
||||
onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode | null
|
||||
onText: OnText,
|
||||
onFinalMessage: OnFinalMessage,
|
||||
chatMode: ChatMode | null,
|
||||
mcpTools: InternalToolInfo[] | undefined,
|
||||
): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => {
|
||||
|
||||
if (!chatMode) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
|
||||
const tools = availableTools(chatMode)
|
||||
const tools = availableTools(chatMode, mcpTools)
|
||||
if (!tools) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
|
||||
|
||||
const toolOfToolName: ToolOfToolName = {}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { AnthropicLLMChatMessage, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMe
|
|||
import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, OverridesOfModel, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
|
||||
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getReservedOutputTokenSpace } from '../../common/modelCapabilities.js';
|
||||
import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGrammar.js';
|
||||
import { availableTools, InternalToolInfo, isAToolName, ToolParamName, voidTools } from '../../common/prompt/prompts.js';
|
||||
import { availableTools, InternalToolInfo } from '../../common/prompt/prompts.js';
|
||||
import { generateUuid } from '../../../../../base/common/uuid.js';
|
||||
|
||||
const getGoogleApiKey = async () => {
|
||||
|
|
@ -48,6 +48,7 @@ type SendChatParams_Internal = InternalCommonMessageParams & {
|
|||
messages: LLMChatMessage[];
|
||||
separateSystemMessage: string | undefined;
|
||||
chatMode: ChatMode | null;
|
||||
mcpTools: InternalToolInfo[] | undefined;
|
||||
}
|
||||
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; separateSystemMessage: string | undefined; }
|
||||
export type ListParams_Internal<ModelResponse> = ModelListParams<ModelResponse>
|
||||
|
|
@ -206,8 +207,8 @@ const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => {
|
|||
} satisfies OpenAI.Chat.Completions.ChatCompletionTool
|
||||
}
|
||||
|
||||
const openAITools = (chatMode: ChatMode) => {
|
||||
const allowedTools = availableTools(chatMode)
|
||||
const openAITools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined) => {
|
||||
const allowedTools = availableTools(chatMode, mcpTools)
|
||||
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
|
||||
|
||||
const openAITools: OpenAI.Chat.Completions.ChatCompletionTool[] = []
|
||||
|
|
@ -217,29 +218,36 @@ const openAITools = (chatMode: ChatMode) => {
|
|||
return openAITools
|
||||
}
|
||||
|
||||
const rawToolCallObjOf = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => {
|
||||
if (!isAToolName(name)) return null
|
||||
const rawParams: RawToolParamsObj = {}
|
||||
|
||||
// convert LLM tool call to our tool format
|
||||
const rawToolCallObjOfParamsStr = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => {
|
||||
let input: unknown
|
||||
try {
|
||||
input = JSON.parse(toolParamsStr)
|
||||
}
|
||||
catch (e) {
|
||||
return null
|
||||
}
|
||||
try { input = JSON.parse(toolParamsStr) }
|
||||
catch (e) { return null }
|
||||
|
||||
if (input === null) return null
|
||||
if (typeof input !== 'object') return null
|
||||
for (const paramName in voidTools[name].params) {
|
||||
rawParams[paramName as ToolParamName] = (input as any)[paramName]
|
||||
}
|
||||
return { id, name, rawParams, doneParams: Object.keys(rawParams) as ToolParamName[], isDone: true }
|
||||
|
||||
const rawParams: RawToolParamsObj = input
|
||||
return { id, name, rawParams, doneParams: Object.keys(rawParams), isDone: true }
|
||||
}
|
||||
|
||||
|
||||
const rawToolCallObjOfAnthropicParams = (toolBlock: Anthropic.Messages.ToolUseBlock): RawToolCallObj | null => {
|
||||
const { id, name, input } = toolBlock
|
||||
|
||||
if (input === null) return null
|
||||
if (typeof input !== 'object') return null
|
||||
|
||||
const rawParams: RawToolParamsObj = input
|
||||
return { id, name, rawParams, doneParams: Object.keys(rawParams), isDone: true }
|
||||
}
|
||||
|
||||
|
||||
// ------------ OPENAI-COMPATIBLE ------------
|
||||
|
||||
|
||||
const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage, overridesOfModel }: SendChatParams_Internal) => {
|
||||
const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage, overridesOfModel, mcpTools }: SendChatParams_Internal) => {
|
||||
const {
|
||||
modelName,
|
||||
specialToolFormat,
|
||||
|
|
@ -259,7 +267,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
|
|||
}
|
||||
|
||||
// tools
|
||||
const potentialTools = chatMode !== null ? openAITools(chatMode) : null
|
||||
const potentialTools = openAITools(chatMode, mcpTools)
|
||||
const nativeToolsObj = potentialTools && specialToolFormat === 'openai-style' ?
|
||||
{ tools: potentialTools } as const
|
||||
: {}
|
||||
|
|
@ -290,7 +298,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
|
|||
|
||||
// manually parse out tool results if XML
|
||||
if (!specialToolFormat) {
|
||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
|
||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode, mcpTools)
|
||||
onText = newOnText
|
||||
onFinalMessage = newOnFinalMessage
|
||||
}
|
||||
|
|
@ -335,7 +343,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
|
|||
onText({
|
||||
fullText: fullTextSoFar,
|
||||
fullReasoning: fullReasoningSoFar,
|
||||
toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined,
|
||||
toolCall: { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId },
|
||||
})
|
||||
|
||||
}
|
||||
|
|
@ -344,7 +352,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
|
|||
onError({ message: 'Void: Response from model was empty.', fullError: null })
|
||||
}
|
||||
else {
|
||||
const toolCall = rawToolCallObjOf(toolName, toolParamsStr, toolId)
|
||||
const toolCall = rawToolCallObjOfParamsStr(toolName, toolParamsStr, toolId)
|
||||
const toolCallObj = toolCall ? { toolCall } : {}
|
||||
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj });
|
||||
}
|
||||
|
|
@ -410,8 +418,8 @@ const toAnthropicTool = (toolInfo: InternalToolInfo) => {
|
|||
} satisfies Anthropic.Messages.Tool
|
||||
}
|
||||
|
||||
const anthropicTools = (chatMode: ChatMode) => {
|
||||
const allowedTools = availableTools(chatMode)
|
||||
const anthropicTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined) => {
|
||||
const allowedTools = availableTools(chatMode, mcpTools)
|
||||
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
|
||||
|
||||
const anthropicTools: Anthropic.Messages.ToolUnion[] = []
|
||||
|
|
@ -421,20 +429,10 @@ const anthropicTools = (chatMode: ChatMode) => {
|
|||
return anthropicTools
|
||||
}
|
||||
|
||||
const anthropicToolToRawToolCallObj = (toolBlock: Anthropic.Messages.ToolUseBlock): RawToolCallObj | null => {
|
||||
const { id, name, input } = toolBlock
|
||||
if (!isAToolName(name)) return null
|
||||
const rawParams: RawToolParamsObj = {}
|
||||
if (input === null) return null
|
||||
if (typeof input !== 'object') return null
|
||||
for (const paramName in voidTools[name].params) {
|
||||
rawParams[paramName as ToolParamName] = (input as any)[paramName]
|
||||
}
|
||||
return { id, name, rawParams, doneParams: Object.keys(rawParams) as ToolParamName[], isDone: true }
|
||||
}
|
||||
|
||||
|
||||
// ------------ ANTHROPIC ------------
|
||||
const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName: modelName_, _setAborter, separateSystemMessage, chatMode }: SendChatParams_Internal) => {
|
||||
const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName: modelName_, _setAborter, separateSystemMessage, chatMode, mcpTools }: SendChatParams_Internal) => {
|
||||
const {
|
||||
modelName,
|
||||
specialToolFormat,
|
||||
|
|
@ -451,7 +449,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
|
|||
const maxTokens = getReservedOutputTokenSpace(providerName, modelName_, { isReasoningEnabled: !!reasoningInfo?.isReasoningEnabled, overridesOfModel })
|
||||
|
||||
// tools
|
||||
const potentialTools = chatMode !== null ? anthropicTools(chatMode) : null
|
||||
const potentialTools = anthropicTools(chatMode, mcpTools)
|
||||
const nativeToolsObj = potentialTools && specialToolFormat === 'anthropic-style' ?
|
||||
{ tools: potentialTools, tool_choice: { type: 'auto' } } as const
|
||||
: {}
|
||||
|
|
@ -475,7 +473,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
|
|||
|
||||
// manually parse out tool results if XML
|
||||
if (!specialToolFormat) {
|
||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
|
||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode, mcpTools)
|
||||
onText = newOnText
|
||||
onFinalMessage = newOnFinalMessage
|
||||
}
|
||||
|
|
@ -492,7 +490,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
|
|||
onText({
|
||||
fullText,
|
||||
fullReasoning,
|
||||
toolCall: isAToolName(fullToolName) ? { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' } : undefined,
|
||||
toolCall: { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' },
|
||||
})
|
||||
}
|
||||
// there are no events for tool_use, it comes in at the end
|
||||
|
|
@ -542,7 +540,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
|
|||
stream.on('finalMessage', (response) => {
|
||||
const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking')
|
||||
const tools = response.content.filter(c => c.type === 'tool_use')
|
||||
const toolCall = tools[0] && anthropicToolToRawToolCallObj(tools[0])
|
||||
const toolCall = tools[0] && rawToolCallObjOfAnthropicParams(tools[0])
|
||||
const toolCallObj = toolCall ? { toolCall } : {}
|
||||
onFinalMessage({ fullText, fullReasoning, anthropicReasoning, ...toolCallObj })
|
||||
})
|
||||
|
|
@ -676,8 +674,8 @@ const toGeminiFunctionDecl = (toolInfo: InternalToolInfo) => {
|
|||
} satisfies FunctionDeclaration
|
||||
}
|
||||
|
||||
const geminiTools = (chatMode: ChatMode): GeminiTool[] | null => {
|
||||
const allowedTools = availableTools(chatMode)
|
||||
const geminiTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined): GeminiTool[] | null => {
|
||||
const allowedTools = availableTools(chatMode, mcpTools)
|
||||
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
|
||||
const functionDecls: FunctionDeclaration[] = []
|
||||
for (const t in allowedTools ?? {}) {
|
||||
|
|
@ -703,6 +701,7 @@ const sendGeminiChat = async ({
|
|||
providerName,
|
||||
modelSelectionOptions,
|
||||
chatMode,
|
||||
mcpTools,
|
||||
}: SendChatParams_Internal) => {
|
||||
|
||||
if (providerName !== 'gemini') throw new Error(`Sending Gemini chat, but provider was ${providerName}`)
|
||||
|
|
@ -728,7 +727,7 @@ const sendGeminiChat = async ({
|
|||
: undefined
|
||||
|
||||
// tools
|
||||
const potentialTools = chatMode !== null ? geminiTools(chatMode) : undefined
|
||||
const potentialTools = geminiTools(chatMode, mcpTools)
|
||||
const toolConfig = potentialTools && specialToolFormat === 'gemini-style' ?
|
||||
potentialTools
|
||||
: undefined
|
||||
|
|
@ -739,7 +738,7 @@ const sendGeminiChat = async ({
|
|||
|
||||
// manually parse out tool results if XML
|
||||
if (!specialToolFormat) {
|
||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
|
||||
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode, mcpTools)
|
||||
onText = newOnText
|
||||
onFinalMessage = newOnFinalMessage
|
||||
}
|
||||
|
|
@ -786,7 +785,7 @@ const sendGeminiChat = async ({
|
|||
onText({
|
||||
fullText: fullTextSoFar,
|
||||
fullReasoning: fullReasoningSoFar,
|
||||
toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined,
|
||||
toolCall: { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId },
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -795,7 +794,7 @@ const sendGeminiChat = async ({
|
|||
onError({ message: 'Void: Response from model was empty.', fullError: null })
|
||||
} else {
|
||||
if (!toolId) toolId = generateUuid() // ids are empty, but other providers might expect an id
|
||||
const toolCall = rawToolCallObjOf(toolName, toolParamsStr, toolId)
|
||||
const toolCall = rawToolCallObjOfParamsStr(toolName, toolParamsStr, toolId)
|
||||
const toolCallObj = toolCall ? { toolCall } : {}
|
||||
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export const sendLLMMessage = async ({
|
|||
overridesOfModel,
|
||||
chatMode,
|
||||
separateSystemMessage,
|
||||
mcpTools,
|
||||
}: SendLLMMessageParams,
|
||||
|
||||
metricsService: IMetricsService
|
||||
|
|
@ -107,7 +108,7 @@ export const sendLLMMessage = async ({
|
|||
}
|
||||
const { sendFIM, sendChat } = implementation
|
||||
if (messagesType === 'chatMessages') {
|
||||
await sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName, _setAborter, providerName, separateSystemMessage, chatMode })
|
||||
await sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName, _setAborter, providerName, separateSystemMessage, chatMode, mcpTools })
|
||||
return
|
||||
}
|
||||
if (messagesType === 'FIMMessage') {
|
||||
|
|
|
|||
411
src/vs/workbench/contrib/void/electron-main/mcpChannel.ts
Normal file
411
src/vs/workbench/contrib/void/electron-main/mcpChannel.ts
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
// registered in app.ts
|
||||
// can't make a service responsible for this, because it needs
|
||||
// to be connected to the main process and node dependencies
|
||||
|
||||
import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||
import { MCPConfigFileJSON, MCPConfigFileEntryJSON, MCPServer, RawMCPToolCall, MCPToolErrorResponse, MCPServerEventResponse, MCPToolCallParams } from '../common/mcpServiceTypes.js';
|
||||
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { MCPUserStateOfName } from '../common/voidSettingsTypes.js';
|
||||
|
||||
const getClientConfig = (serverName: string) => {
|
||||
return {
|
||||
name: `${serverName}-client`,
|
||||
version: '0.1.0',
|
||||
// debug: true,
|
||||
}
|
||||
}
|
||||
|
||||
type MCPServerNonError = MCPServer & { status: Omit<MCPServer['status'], 'error'> }
|
||||
type MCPServerError = MCPServer & { status: 'error' }
|
||||
|
||||
|
||||
|
||||
type ClientInfo = {
|
||||
_client: Client, // _client is the client that connects with an mcp client. We're calling mcp clients "server" everywhere except here for naming consistency.
|
||||
mcpServerEntryJSON: MCPConfigFileEntryJSON,
|
||||
mcpServer: MCPServerNonError,
|
||||
} | {
|
||||
_client?: undefined,
|
||||
mcpServerEntryJSON: MCPConfigFileEntryJSON,
|
||||
mcpServer: MCPServerError,
|
||||
}
|
||||
|
||||
type InfoOfClientId = {
|
||||
[clientId: string]: ClientInfo
|
||||
}
|
||||
|
||||
export class MCPChannel implements IServerChannel {
|
||||
|
||||
private readonly infoOfClientId: InfoOfClientId = {}
|
||||
private readonly _refreshingServerNames: Set<string> = new Set()
|
||||
|
||||
// mcp emitters
|
||||
private readonly mcpEmitters = {
|
||||
serverEvent: {
|
||||
onAdd: new Emitter<MCPServerEventResponse>(),
|
||||
onUpdate: new Emitter<MCPServerEventResponse>(),
|
||||
onDelete: new Emitter<MCPServerEventResponse>(),
|
||||
}
|
||||
} satisfies {
|
||||
serverEvent: {
|
||||
onAdd: Emitter<MCPServerEventResponse>,
|
||||
onUpdate: Emitter<MCPServerEventResponse>,
|
||||
onDelete: Emitter<MCPServerEventResponse>,
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
) { }
|
||||
|
||||
// browser uses this to listen for changes
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
|
||||
// server events
|
||||
if (event === 'onAdd_server') return this.mcpEmitters.serverEvent.onAdd.event;
|
||||
else if (event === 'onUpdate_server') return this.mcpEmitters.serverEvent.onUpdate.event;
|
||||
else if (event === 'onDelete_server') return this.mcpEmitters.serverEvent.onDelete.event;
|
||||
// else if (event === 'onLoading_server') return this.mcpEmitters.serverEvent.onChangeLoading.event;
|
||||
|
||||
// tool call events
|
||||
|
||||
// handle unknown events
|
||||
else throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
// browser uses this to call (see this.channel.call() in mcpConfigService.ts for all usages)
|
||||
async call(_: unknown, command: string, params: any): Promise<any> {
|
||||
try {
|
||||
if (command === 'refreshMCPServers') {
|
||||
await this._refreshMCPServers(params)
|
||||
}
|
||||
else if (command === 'closeAllMCPServers') {
|
||||
await this._closeAllMCPServers()
|
||||
}
|
||||
else if (command === 'toggleMCPServer') {
|
||||
await this._toggleMCPServer(params.serverName, params.isOn)
|
||||
}
|
||||
else if (command === 'callTool') {
|
||||
const p: MCPToolCallParams = params
|
||||
const response = await this._safeCallTool(p.serverName, p.toolName, p.params)
|
||||
return response
|
||||
}
|
||||
else {
|
||||
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('mcp channel: Call Error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// server functions
|
||||
|
||||
|
||||
private async _refreshMCPServers(params: { mcpConfigFileJSON: MCPConfigFileJSON, userStateOfName: MCPUserStateOfName, addedServerNames: string[], removedServerNames: string[], updatedServerNames: string[] }) {
|
||||
|
||||
const {
|
||||
mcpConfigFileJSON,
|
||||
userStateOfName,
|
||||
addedServerNames,
|
||||
removedServerNames,
|
||||
updatedServerNames,
|
||||
} = params
|
||||
|
||||
const { mcpServers: mcpServersJSON } = mcpConfigFileJSON
|
||||
|
||||
const allChanges: { type: 'added' | 'removed' | 'updated', serverName: string }[] = [
|
||||
...addedServerNames.map(n => ({ serverName: n, type: 'added' }) as const),
|
||||
...removedServerNames.map(n => ({ serverName: n, type: 'removed' }) as const),
|
||||
...updatedServerNames.map(n => ({ serverName: n, type: 'updated' }) as const),
|
||||
]
|
||||
|
||||
await Promise.all(
|
||||
allChanges.map(async ({ serverName, type }) => {
|
||||
|
||||
// check if already refreshing
|
||||
if (this._refreshingServerNames.has(serverName)) return
|
||||
this._refreshingServerNames.add(serverName)
|
||||
|
||||
const prevServer = this.infoOfClientId[serverName]?.mcpServer;
|
||||
|
||||
// close and delete the old client
|
||||
if (type === 'removed' || type === 'updated') {
|
||||
await this._closeClient(serverName)
|
||||
delete this.infoOfClientId[serverName]
|
||||
this.mcpEmitters.serverEvent.onDelete.fire({ response: { prevServer, name: serverName, } })
|
||||
}
|
||||
|
||||
// create a new client
|
||||
if (type === 'added' || type === 'updated') {
|
||||
const clientInfo = await this._createClient(mcpServersJSON[serverName], serverName, userStateOfName[serverName]?.isOn)
|
||||
this.infoOfClientId[serverName] = clientInfo
|
||||
this.mcpEmitters.serverEvent.onAdd.fire({ response: { newServer: clientInfo.mcpServer, name: serverName, } })
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
allChanges.forEach(({ serverName, type }) => {
|
||||
this._refreshingServerNames.delete(serverName)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private async _createClientUnsafe(server: MCPConfigFileEntryJSON, serverName: string, isOn: boolean): Promise<ClientInfo> {
|
||||
|
||||
const clientConfig = getClientConfig(serverName)
|
||||
const client = new Client(clientConfig)
|
||||
let transport: Transport;
|
||||
let info: MCPServerNonError;
|
||||
|
||||
if (server.url) {
|
||||
// first try HTTP, fall back to SSE
|
||||
try {
|
||||
transport = new StreamableHTTPClientTransport(server.url);
|
||||
await client.connect(transport);
|
||||
console.log(`Connected via HTTP to ${serverName}`);
|
||||
const { tools } = await client.listTools()
|
||||
const toolsWithUniqueName = tools.map(({ name, ...rest }) => ({ name: this._addUniquePrefix(name), ...rest }))
|
||||
info = {
|
||||
status: isOn ? 'success' : 'offline',
|
||||
tools: toolsWithUniqueName,
|
||||
command: server.url.toString(),
|
||||
}
|
||||
} catch (httpErr) {
|
||||
console.warn(`HTTP failed for ${serverName}, trying SSE…`, httpErr);
|
||||
transport = new SSEClientTransport(server.url);
|
||||
await client.connect(transport);
|
||||
const { tools } = await client.listTools()
|
||||
const toolsWithUniqueName = tools.map(({ name, ...rest }) => ({ name: this._addUniquePrefix(name), ...rest }))
|
||||
console.log(`Connected via SSE to ${serverName}`);
|
||||
info = {
|
||||
status: isOn ? 'success' : 'offline',
|
||||
tools: toolsWithUniqueName,
|
||||
command: server.url.toString(),
|
||||
}
|
||||
}
|
||||
} else if (server.command) {
|
||||
console.log('ENV DATA: ', server.env)
|
||||
transport = new StdioClientTransport({
|
||||
command: server.command,
|
||||
args: server.args,
|
||||
env: {
|
||||
...server.env,
|
||||
...process.env
|
||||
} as Record<string, string>,
|
||||
});
|
||||
|
||||
await client.connect(transport)
|
||||
|
||||
// Get the tools from the server
|
||||
const { tools } = await client.listTools()
|
||||
const toolsWithUniqueName = tools.map(({ name, ...rest }) => ({ name: this._addUniquePrefix(name), ...rest }))
|
||||
|
||||
// Create a full command string for display
|
||||
const fullCommand = `${server.command} ${server.args?.join(' ') || ''}`
|
||||
|
||||
// Format server object
|
||||
info = {
|
||||
status: isOn ? 'success' : 'offline',
|
||||
tools: toolsWithUniqueName,
|
||||
command: fullCommand,
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(`No url or command for server ${serverName}`);
|
||||
}
|
||||
|
||||
|
||||
return { _client: client, mcpServerEntryJSON: server, mcpServer: info }
|
||||
}
|
||||
|
||||
private _addUniquePrefix(base: string) {
|
||||
return `${Math.random().toString(36).slice(2, 8)}_${base}`;
|
||||
}
|
||||
|
||||
private async _createClient(serverConfig: MCPConfigFileEntryJSON, serverName: string, isOn = true): Promise<ClientInfo> {
|
||||
try {
|
||||
const c: ClientInfo = await this._createClientUnsafe(serverConfig, serverName, isOn)
|
||||
return c
|
||||
} catch (err) {
|
||||
console.error(`❌ Failed to connect to server "${serverName}":`, err)
|
||||
const fullCommand = !serverConfig.command ? '' : `${serverConfig.command} ${serverConfig.args?.join(' ') || ''}`
|
||||
const c: MCPServerError = { status: 'error', error: err + '', command: fullCommand, }
|
||||
return { mcpServerEntryJSON: serverConfig, mcpServer: c, }
|
||||
}
|
||||
}
|
||||
|
||||
private async _closeAllMCPServers() {
|
||||
for (const serverName in this.infoOfClientId) {
|
||||
await this._closeClient(serverName)
|
||||
delete this.infoOfClientId[serverName]
|
||||
}
|
||||
console.log('Closed all MCP servers');
|
||||
}
|
||||
|
||||
private async _closeClient(serverName: string) {
|
||||
const info = this.infoOfClientId[serverName]
|
||||
if (!info) return
|
||||
const { _client: client } = info
|
||||
if (client) {
|
||||
await client.close()
|
||||
}
|
||||
console.log(`Closed MCP server ${serverName}`);
|
||||
}
|
||||
|
||||
|
||||
private async _toggleMCPServer(serverName: string, isOn: boolean) {
|
||||
const prevServer = this.infoOfClientId[serverName]?.mcpServer
|
||||
// Handle turning on the server
|
||||
if (isOn) {
|
||||
// this.mcpEmitters.serverEvent.onChangeLoading.fire(getLoadingServerObject(serverName, isOn))
|
||||
const clientInfo = await this._createClientUnsafe(this.infoOfClientId[serverName].mcpServerEntryJSON, serverName, isOn)
|
||||
this.mcpEmitters.serverEvent.onUpdate.fire({
|
||||
response: {
|
||||
name: serverName,
|
||||
newServer: clientInfo.mcpServer,
|
||||
prevServer: prevServer,
|
||||
}
|
||||
})
|
||||
}
|
||||
// Handle turning off the server
|
||||
else {
|
||||
// this.mcpEmitters.serverEvent.onChangeLoading.fire(getLoadingServerObject(serverName, isOn))
|
||||
this._closeClient(serverName)
|
||||
delete this.infoOfClientId[serverName]._client
|
||||
|
||||
this.mcpEmitters.serverEvent.onUpdate.fire({
|
||||
response: {
|
||||
name: serverName,
|
||||
newServer: {
|
||||
status: 'offline',
|
||||
tools: [],
|
||||
command: '',
|
||||
// Explicitly set error to undefined to reset the error state
|
||||
error: undefined,
|
||||
},
|
||||
prevServer: prevServer,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// tool call functions
|
||||
|
||||
private async _callTool(serverName: string, toolName: string, params: any): Promise<RawMCPToolCall> {
|
||||
const server = this.infoOfClientId[serverName]
|
||||
if (!server) throw new Error(`Server ${serverName} not found`)
|
||||
const { _client: client } = server
|
||||
if (!client) throw new Error(`Client for server ${serverName} not found`)
|
||||
|
||||
// Call the tool with the provided parameters
|
||||
const response = await client.callTool({
|
||||
name: toolName,
|
||||
arguments: params
|
||||
})
|
||||
const { content } = response as CallToolResult
|
||||
const returnValue = content[0]
|
||||
|
||||
if (returnValue.type === 'text') {
|
||||
// handle text response
|
||||
|
||||
if (response.isError) {
|
||||
throw new Error(`Tool call error: ${response.content}`)
|
||||
// handle error
|
||||
}
|
||||
|
||||
// handle success
|
||||
return {
|
||||
event: 'text',
|
||||
text: returnValue.text,
|
||||
toolName,
|
||||
serverName,
|
||||
}
|
||||
}
|
||||
|
||||
// if (returnValue.type === 'audio') {
|
||||
// // handle audio response
|
||||
// }
|
||||
|
||||
// if (returnValue.type === 'image') {
|
||||
// // handle image response
|
||||
// }
|
||||
|
||||
// if (returnValue.type === 'resource') {
|
||||
// // handle resource response
|
||||
// }
|
||||
|
||||
throw new Error(`Tool call error: We don\'t support ${returnValue.type} tool response yet for tool ${toolName} on server ${serverName}`)
|
||||
}
|
||||
|
||||
// tool call error wrapper
|
||||
private async _safeCallTool(serverName: string, toolName: string, params: any): Promise<RawMCPToolCall> {
|
||||
try {
|
||||
const response = await this._callTool(serverName, toolName, params)
|
||||
return response
|
||||
} catch (err) {
|
||||
let errorMessage: string;
|
||||
|
||||
// Check if it's an MCP error with a code
|
||||
if (err && typeof err === 'object' && 'code' in err) {
|
||||
const errorCode = err.code;
|
||||
const errorName = err.name || 'Unknown Error';
|
||||
const errorMsg = err.message || '';
|
||||
|
||||
// Map common JSON-RPC error codes to user-friendly messages
|
||||
let codeDescription = '';
|
||||
switch (errorCode) {
|
||||
case -32700:
|
||||
codeDescription = 'Parse Error';
|
||||
break;
|
||||
case -32600:
|
||||
codeDescription = 'Invalid Request';
|
||||
break;
|
||||
case -32601:
|
||||
codeDescription = 'Method Not Found';
|
||||
break;
|
||||
case -32602:
|
||||
codeDescription = 'Invalid Parameters';
|
||||
break;
|
||||
case -32603:
|
||||
codeDescription = 'Internal Error';
|
||||
break;
|
||||
default:
|
||||
codeDescription = `Error Code ${errorCode}`;
|
||||
}
|
||||
|
||||
errorMessage = `${errorName} (${codeDescription})${errorMsg ? ': ' + errorMsg : ''}`;
|
||||
} else if (err && typeof err === 'object' && 'message' in err) {
|
||||
// Standard error with message
|
||||
errorMessage = err.message;
|
||||
} else if (typeof err === 'string') {
|
||||
// String error
|
||||
errorMessage = err;
|
||||
} else {
|
||||
// Unknown error format
|
||||
errorMessage = JSON.stringify(err, null, 2);
|
||||
}
|
||||
|
||||
const fullErrorMessage = `❌ Failed to call tool "${toolName}" on server "${serverName}": ${errorMessage}`;
|
||||
const errorResponse: MCPToolErrorResponse = {
|
||||
event: 'error',
|
||||
text: fullErrorMessage,
|
||||
toolName,
|
||||
serverName,
|
||||
}
|
||||
return errorResponse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in a new issue