mirror of
https://github.com/EthanMarti/infio-copilot.git
synced 2026-05-11 17:53:11 +00:00
add buildin mcp server
This commit is contained in:
@@ -21,8 +21,9 @@ import deepEqual from "fast-deep-equal"; // Keep fast-deep-equal
|
||||
import ReconnectingEventSource from "reconnecting-eventsource"; // Keep reconnecting-eventsource
|
||||
import { EnvironmentVariables, shellEnvSync } from 'shell-env';
|
||||
import { z } from "zod"; // Keep zod
|
||||
|
||||
// Internal/Project imports
|
||||
|
||||
import { INFIO_BASE_URL } from '../../constants'
|
||||
import { t } from "../../lang/helpers";
|
||||
import InfioPlugin from "../../main";
|
||||
// Assuming path is correct and will be resolved, if not, this will remain an error.
|
||||
@@ -45,6 +46,14 @@ export type McpConnection = {
|
||||
transport: StdioClientTransport | SSEClientTransport
|
||||
}
|
||||
|
||||
// 添加内置服务器连接类型
|
||||
export type BuiltInMcpConnection = {
|
||||
server: McpServer
|
||||
// 内置服务器不需要 client 和 transport,直接通过 HTTP API 调用
|
||||
}
|
||||
|
||||
export type AllMcpConnection = McpConnection | BuiltInMcpConnection
|
||||
|
||||
// Base configuration schema for common settings
|
||||
const BaseConfigSchema = z.object({
|
||||
disabled: z.boolean().optional(),
|
||||
@@ -114,6 +123,16 @@ const McpSettingsSchema = z.object({
|
||||
mcpServers: z.record(ServerConfigSchema),
|
||||
})
|
||||
|
||||
// 内置服务器工具的 API 响应类型
|
||||
interface BuiltInToolResponse {
|
||||
name: string
|
||||
description?: string
|
||||
inputSchema?: object
|
||||
mcp_info?: {
|
||||
server_name: string
|
||||
}
|
||||
}
|
||||
|
||||
export class McpHub {
|
||||
private app: App
|
||||
private plugin: InfioPlugin
|
||||
@@ -122,12 +141,17 @@ export class McpHub {
|
||||
private fileWatchers: Map<string, FSWatcher[]> = new Map()
|
||||
private isDisposed: boolean = false
|
||||
connections: McpConnection[] = []
|
||||
// 添加内置服务器连接
|
||||
builtInConnection: BuiltInMcpConnection | null = null
|
||||
isConnecting: boolean = false
|
||||
private refCount: number = 0 // Reference counter for active clients
|
||||
private eventRefs: EventRef[] = []; // For managing Obsidian event listeners
|
||||
// private providerRef: any; // TODO: Replace with actual type and initialize properly. Removed for now as it causes issues and its usage is unclear in the current scope.
|
||||
private shellEnv: EnvironmentVariables
|
||||
|
||||
// 内置服务器配置
|
||||
private readonly BUILTIN_SERVER_NAME = "infio-builtin-server"
|
||||
|
||||
constructor(app: App, plugin: InfioPlugin) {
|
||||
this.app = app
|
||||
this.plugin = plugin
|
||||
@@ -144,6 +168,8 @@ export class McpHub {
|
||||
await this.watchMcpSettingsFile();
|
||||
// this.setupWorkspaceWatcher();
|
||||
await this.initializeGlobalMcpServers();
|
||||
// 初始化内置服务器
|
||||
await this.initializeBuiltInServer();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,7 +202,9 @@ export class McpHub {
|
||||
if (typeof config !== 'object' || config === null) {
|
||||
throw new Error("Server configuration must be an object.");
|
||||
}
|
||||
const configObj = config as Record<string, unknown>; // Cast after check
|
||||
|
||||
// 使用类型保护而不是类型断言
|
||||
const configObj = config as Record<string, unknown>;
|
||||
|
||||
// Detect configuration issues before validation
|
||||
const hasStdioFields = configObj.command !== undefined
|
||||
@@ -278,12 +306,26 @@ export class McpHub {
|
||||
|
||||
getServers(): McpServer[] {
|
||||
// Only return enabled servers
|
||||
return this.connections.filter((conn) => !conn.server.disabled).map((conn) => conn.server)
|
||||
const standardServers = this.connections.filter((conn) => !conn.server.disabled).map((conn) => conn.server)
|
||||
|
||||
// 添加内置服务器(如果存在且未禁用)
|
||||
if (this.builtInConnection && !this.builtInConnection.server.disabled) {
|
||||
return [this.builtInConnection.server, ...standardServers]
|
||||
}
|
||||
|
||||
return standardServers
|
||||
}
|
||||
|
||||
getAllServers(): McpServer[] {
|
||||
// Return all servers regardless of state
|
||||
return this.connections.map((conn) => conn.server)
|
||||
const standardServers = this.connections.map((conn) => conn.server)
|
||||
|
||||
// 添加内置服务器(如果存在)
|
||||
if (this.builtInConnection) {
|
||||
return [this.builtInConnection.server, ...standardServers]
|
||||
}
|
||||
|
||||
return standardServers
|
||||
}
|
||||
|
||||
async ensureMcpFileExists(): Promise<void> {
|
||||
@@ -347,9 +389,13 @@ export class McpHub {
|
||||
new Notice(String(t("common:errors.invalid_mcp_settings_validation")) + ": " + errorMessages);
|
||||
// Still try to connect with the raw config for global, but show warnings
|
||||
try {
|
||||
// Ensure config.mcpServers is treated as the correct type for updateServerConnections
|
||||
const serversToConnect = config.mcpServers as Record<string, Partial<z.infer<typeof ServerConfigSchema>>> | undefined;
|
||||
await this.updateServerConnections(serversToConnect || {} as Record<string, Partial<z.infer<typeof ServerConfigSchema>>>);
|
||||
// 安全地处理未验证的配置
|
||||
const serversToConnect = config.mcpServers;
|
||||
if (serversToConnect && typeof serversToConnect === 'object') {
|
||||
await this.updateServerConnections(serversToConnect);
|
||||
} else {
|
||||
await this.updateServerConnections({});
|
||||
}
|
||||
} catch (error) {
|
||||
this.showErrorMessage(`Failed to initialize MCP servers with raw config`, error);
|
||||
}
|
||||
@@ -393,9 +439,7 @@ export class McpHub {
|
||||
let configInjected = { ...config };
|
||||
try {
|
||||
// injectEnv might return a modified structure, so we re-validate.
|
||||
// config is z.infer<typeof ServerConfigSchema>, injectEnv expects Record<string, any>
|
||||
// This assumes injectEnv can handle the structure of ServerConfigSchema or its parts.
|
||||
const tempConfigAfterInject = await injectEnv(config as any);
|
||||
const tempConfigAfterInject = await injectEnv(config as Record<string, unknown>);
|
||||
const validatedInjectedConfig = ServerConfigSchema.safeParse(tempConfigAfterInject);
|
||||
if (validatedInjectedConfig.success) {
|
||||
configInjected = validatedInjectedConfig.data;
|
||||
@@ -501,7 +545,7 @@ export class McpHub {
|
||||
const connection = this.findConnection(name, source)
|
||||
if (connection) {
|
||||
connection.server.status = "disconnected"
|
||||
this.appendErrorMessage(connection, error instanceof Error ? error.message : `${error}`)
|
||||
this.appendErrorMessage(connection, error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
// await this.notifyWebviewOfServerChanges()
|
||||
}
|
||||
@@ -708,7 +752,7 @@ export class McpHub {
|
||||
}
|
||||
|
||||
async updateServerConnections(
|
||||
newServers: Record<string, any>,
|
||||
newServers: Record<string, unknown>,
|
||||
source: "global" | "project" = "global",
|
||||
): Promise<void> {
|
||||
this.isConnecting = true
|
||||
@@ -926,6 +970,15 @@ export class McpHub {
|
||||
source: "global" | "project" = "global",
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 检查是否为内置服务器
|
||||
if (serverName === this.BUILTIN_SERVER_NAME) {
|
||||
if (this.builtInConnection) {
|
||||
this.builtInConnection.server.disabled = disabled
|
||||
console.log(`Built-in server ${disabled ? 'disabled' : 'enabled'}`)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Find the connection to determine if it's a global or project server
|
||||
const connection = this.findConnection(serverName, source)
|
||||
if (!connection) {
|
||||
@@ -968,7 +1021,7 @@ export class McpHub {
|
||||
*/
|
||||
private async updateServerConfig(
|
||||
serverName: string,
|
||||
configUpdate: Record<string, any>,
|
||||
configUpdate: Record<string, unknown>,
|
||||
source: "global" | "project" = "global",
|
||||
): Promise<void> {
|
||||
// Determine which config file to update
|
||||
@@ -1082,7 +1135,8 @@ export class McpHub {
|
||||
|
||||
// Remove the server from the settings
|
||||
if (config.mcpServers[serverName]) {
|
||||
delete config.mcpServers[serverName]
|
||||
// 使用 Reflect.deleteProperty 而不是 delete 操作符
|
||||
Reflect.deleteProperty(config.mcpServers, serverName)
|
||||
|
||||
// Write the entire config back
|
||||
const updatedConfig = {
|
||||
@@ -1208,6 +1262,11 @@ export class McpHub {
|
||||
toolArguments?: Record<string, unknown>,
|
||||
source: "global" | "project" = "global",
|
||||
): Promise<McpToolCallResponse> {
|
||||
// 检查是否为内置服务器
|
||||
if (serverName === this.BUILTIN_SERVER_NAME) {
|
||||
return await this.callBuiltInTool(toolName, toolArguments)
|
||||
}
|
||||
|
||||
const connection = this.findConnection(serverName, source)
|
||||
if (!connection) {
|
||||
throw new Error(
|
||||
@@ -1244,6 +1303,63 @@ export class McpHub {
|
||||
)
|
||||
}
|
||||
|
||||
// 调用内置服务器工具
|
||||
private async callBuiltInTool(
|
||||
toolName: string,
|
||||
toolArguments?: Record<string, unknown>
|
||||
): Promise<McpToolCallResponse> {
|
||||
try {
|
||||
if (!this.builtInConnection) {
|
||||
throw new Error("Built-in server is not initialized")
|
||||
}
|
||||
|
||||
if (this.builtInConnection.server.disabled) {
|
||||
throw new Error("Built-in server is disabled and cannot be used")
|
||||
}
|
||||
|
||||
if (this.builtInConnection.server.status !== "connected") {
|
||||
throw new Error("Built-in server is not connected")
|
||||
}
|
||||
|
||||
// 调用内置 API
|
||||
const response = await fetch(`${INFIO_BASE_URL}/mcp/tools/call`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.plugin.settings.infioProvider.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: toolName,
|
||||
arguments: toolArguments || {},
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
// 转换为 McpToolCallResponse 格式
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
|
||||
}],
|
||||
isError: false,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to call built-in tool ${toolName}:`, error)
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Error calling built-in tool: ${error instanceof Error ? error.message : String(error)}`
|
||||
}],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async toggleToolAlwaysAllow(
|
||||
serverName: string,
|
||||
source: "global" | "project" = "global",
|
||||
@@ -1251,6 +1367,19 @@ export class McpHub {
|
||||
shouldAllow: boolean,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 检查是否为内置服务器
|
||||
if (serverName === this.BUILTIN_SERVER_NAME) {
|
||||
if (this.builtInConnection) {
|
||||
// 更新内置服务器工具的 alwaysAllow 状态
|
||||
const tool = this.builtInConnection.server.tools?.find(t => t.name === toolName)
|
||||
if (tool) {
|
||||
tool.alwaysAllow = shouldAllow
|
||||
console.log(`Built-in tool ${toolName} ${shouldAllow ? 'always allowed' : 'permission required'}`)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Find the connection with matching name and source
|
||||
const connection = this.findConnection(serverName, source)
|
||||
|
||||
@@ -1342,7 +1471,80 @@ export class McpHub {
|
||||
}
|
||||
}
|
||||
this.connections = []
|
||||
|
||||
// 清理内置服务器连接
|
||||
this.builtInConnection = null
|
||||
|
||||
this.eventRefs.forEach((ref) => this.app.vault.offref(ref))
|
||||
this.eventRefs = []
|
||||
}
|
||||
|
||||
// 初始化内置服务器
|
||||
private async initializeBuiltInServer(): Promise<void> {
|
||||
try {
|
||||
console.log("Initializing built-in server...")
|
||||
|
||||
// 获取工具列表
|
||||
const tools = await this.fetchBuiltInTools()
|
||||
|
||||
// 创建内置服务器连接
|
||||
this.builtInConnection = {
|
||||
server: {
|
||||
name: this.BUILTIN_SERVER_NAME,
|
||||
config: JSON.stringify({ type: "builtin" }),
|
||||
status: "connected",
|
||||
disabled: false,
|
||||
source: "global",
|
||||
tools: tools,
|
||||
resources: [], // 内置服务器暂不支持资源
|
||||
resourceTemplates: [], // 内置服务器暂不支持资源模板
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Built-in server initialized with ${tools.length} tools`)
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize built-in server:", error)
|
||||
this.builtInConnection = {
|
||||
server: {
|
||||
name: this.BUILTIN_SERVER_NAME,
|
||||
config: JSON.stringify({ type: "builtin" }),
|
||||
status: "disconnected",
|
||||
disabled: false,
|
||||
source: "global",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
tools: [],
|
||||
resources: [],
|
||||
resourceTemplates: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从内置 API 获取工具列表
|
||||
private async fetchBuiltInTools(): Promise<McpTool[]> {
|
||||
try {
|
||||
const response = await fetch(`${INFIO_BASE_URL}/mcp/tools/list`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.plugin.settings.infioProvider.apiKey}`,
|
||||
},
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const tools: BuiltInToolResponse[] = await response.json()
|
||||
|
||||
// 转换为 McpTool 格式
|
||||
return tools.map((tool) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
alwaysAllow: false, // 默认不自动允许
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch built-in tools:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user