add buildin mcp server

This commit is contained in:
duanfuxiang
2025-06-07 14:01:22 +08:00
parent 8915b84b04
commit 8e5a1c75f6
2 changed files with 637 additions and 14 deletions

View File

@@ -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
}
}
}