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

View File

@ -0,0 +1,421 @@
export const BUILTIN_TOOLS = [
{
"name": "CONVERT_DOCUMENT",
"description": "Convert a document to markdown using the Marker API. Supports PDF, Word, Excel, PowerPoint, HTML, and EPUB files.",
"inputSchema": {
"type": "object",
"properties": {
"file_content": {
"type": "string",
"description": "Base64 encoded file content"
},
"file_type": {
"type": "string",
"description": "File type extension (pdf, docx, xlsx, etc.) without the dot"
}
},
"required": [
"file_content",
"file_type"
]
},
"mcp_info": {
"server_name": "internal"
}
},
{
"name": "CONVERT_VIDEO",
"description": "Convert a video url, like youtube url to markdown. Supports youtube, bilibili, tiktok, douyin, kuaishou, etc.",
"inputSchema": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "Video url, like youtube url, bilibili url, tiktok url, douyin url, kuaishou url, etc."
},
"detect_language": {
"type": "string",
"description": "Detect language of the video, like en, zh, etc."
}
},
"required": [
"url",
"detect_language"
]
},
"mcp_info": {
"server_name": "internal"
}
},
{
"name": "COMPOSIO_SEARCH_DUCK_DUCK_GO_SEARCH",
"description": "The duckduckgosearch class utilizes the composio duckduckgo search api to perform searches, focusing on web information and details. it leverages the duckduckgo search engine via the composio duckduckgo search api to retrieve relevant web data based on the provided query.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query for the Composio DuckDuckGo Search API, specifying the search topic."
}
},
"required": [
"query"
],
"additionalProperties": false
},
"mcp_info": {
"server_name": "search"
}
},
{
"name": "COMPOSIO_SEARCH_EVENT_SEARCH",
"description": "The eventsearch class enables scraping of google events search queries. it conducts an event search using the composio events search api, retrieving information on events such as concerts, festivals, and other activities based on the provided query.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query for the Composio Events Search API, specifying the event topic."
}
},
"required": [
"query"
],
"additionalProperties": false
},
"mcp_info": {
"server_name": "search"
}
},
{
"name": "COMPOSIO_SEARCH_EXA_SIMILARLINK",
"description": "Perform a search to find similar links and retrieve a list of relevant results. the search can optionally return contents.",
"inputSchema": {
"type": "object",
"properties": {
"category": {
"type": "string",
"description": " A data category to focus on, with higher comprehensivity and data cleanliness. Categories right now include company, research paper, news, github, tweet, movie, song, personal site, and pdf"
},
"endCrawlDate": {
"type": "string",
"description": "Results will include links crawled before this date. For e.g. '2023-01-01T00:00:00Z', '2023-01-15T00:00:00Z', '2023-02-01T00:00:00Z'."
},
"endPublishedDate": {
"type": "string",
"description": "Only links published before this date will be returned. For e.g. '2023-01-01', '2023-01-15', '2023-02-01'."
},
"excludeDomains": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of domains to exclude in the search. For e.g. ['example.com'], ['news.com'], ['blog.com']."
},
"includeDomains": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of domains to include in the search. For e.g. ['example.com'], ['news.com'], ['blog.com']."
},
"numResults": {
"type": "integer",
"description": "Number of search results to return. For e.g. 10, 20, 30."
},
"startCrawlDate": {
"type": "string",
"description": "Results will include links crawled after this date. For e.g. '2023-01-01T00:00:00Z', '2023-01-15T00:00:00Z', '2023-02-01T00:00:00Z'."
},
"startPublishedDate": {
"type": "string",
"description": "Only links published after this date will be returned. For e.g. '2023-01-01', '2023-01-15', '2023-02-01'."
},
"type": {
"type": "string",
"description": "The type of search: 'keyword', 'neural', or 'magic'. For e.g. 'neural', 'keyword', 'magic'."
},
"url": {
"type": "string",
"description": "The url for which you would like to find similar links. For e.g. 'https://slatestarcodex.com/2014/07/30/meditations-on-moloch/', 'https://ww.google.com/'"
},
"useAutoprompt": {
"type": "boolean",
"description": "If true, your query will be converted to an Composio Similarlinks query. For e.g. True, False, True."
}
},
"required": [
"url"
],
"additionalProperties": false
},
"mcp_info": {
"server_name": "search"
}
},
{
"name": "COMPOSIO_SEARCH_FINANCE_SEARCH",
"description": "The financesearch class utilizes the composio finance search api to conduct financial searches, focusing on financial data and stock information. it leverages the google finance search engine via the composio finance search api to retrieve pertinent financial details based on the provided query.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query for the Composio Finance Search API, specifying the financial topic or stock symbol."
}
},
"required": [
"query"
],
"additionalProperties": false
},
"mcp_info": {
"server_name": "search"
}
},
{
"name": "COMPOSIO_SEARCH_GOOGLE_MAPS_SEARCH",
"description": "The googlemapssearch class performs a location-specific search using the composio goolge maps search api. this class extends the functionality of the base action class to specifically target locations related to the given query. by utilizing the google maps search engine through the composio goolge maps search api, it fetches the most relevant location data based on the input query. the `googlemapssearch` class is particularly useful for applications that need to retrieve and display location information about a specific area. it leverages the powerful search capabilities of google's maps search engine, ensuring that the returned results are accurate and relevant.",
"inputSchema": {
"type": "object",
"properties": {
"ll": {
"type": "string",
"description": "GPS coordinates of location where you want your query to be applied."
},
"q": {
"type": "string",
"description": "The query you want to search."
},
"start": {
"type": "integer",
"description": "Used for pagenation"
}
},
"required": [
"q"
],
"additionalProperties": false
},
"mcp_info": {
"server_name": "search"
}
},
{
"name": "COMPOSIO_SEARCH_IMAGE_SEARCH",
"description": "The imagesearch class performs an image search using the composio image search api, to target image data and information. it uses the google images search engine through the composio image search api to fetch relevant image information based on the input query.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query for the Composio Image Search API, specifying the image topic."
}
},
"required": [
"query"
],
"additionalProperties": false
},
"mcp_info": {
"server_name": "search"
}
},
{
"name": "COMPOSIO_SEARCH_NEWS_SEARCH",
"description": "The newssearch class performs a news-specific search using the composio news search api. this class extends the functionality of the base action class to specifically target news articles related to the given query. by utilizing the google news search engine through the composio news search api, it fetches the most relevant news articles based on the input query. the `newssearch` class is particularly useful for applications that need to retrieve and display the latest news articles about a specific topic. it leverages the powerful search capabilities of google's news search engine, ensuring that the returned results are current and relevant.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query for the Composio News Search API, specifying the topic for news search."
}
},
"required": [
"query"
],
"additionalProperties": false
},
"mcp_info": {
"server_name": "search"
}
},
{
"name": "COMPOSIO_SEARCH_SCHOLAR_SEARCH",
"description": "Scholar api allows you to scrape results from a google scholar search query. the scholarsearch class performs an academic search using the composio scholar search api, academic papers and scholarly articles. it uses the google scholar search engine through the serp api to fetch relevant academic information based on the input query.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query for the Composio Scholar Search API, specifying the academic topic or paper title."
}
},
"required": [
"query"
],
"additionalProperties": false
},
"mcp_info": {
"server_name": "search"
}
},
{
"name": "COMPOSIO_SEARCH_SEARCH",
"description": "Perform a google search using the composio google search api.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query for the Composio Google Search API."
}
},
"required": [
"query"
],
"additionalProperties": false
},
"mcp_info": {
"server_name": "search"
}
},
{
"name": "COMPOSIO_SEARCH_SHOPPING_SEARCH",
"description": "The shoppingsearch class performs a product search using the composio shopping search api.it specifically target shopping results related to the given query. by utilizing the google shopping search engine through the composio shopping search api, it fetches the most relevant product listings based on the input query. the `shoppingsearch` class is particularly useful for applications that need to retrieve and display the latest product listings and shopping results for a specific item. it leverages the powerful search capabilities of google's shopping search engine, ensuring that the returned results are current and relevant.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query for the Composio Shopping Search API, specifying the product or item for shopping search."
}
},
"required": [
"query"
],
"additionalProperties": false
},
"mcp_info": {
"server_name": "search"
}
},
{
"name": "COMPOSIO_SEARCH_TAVILY_SEARCH",
"description": "The composio llm search class serves as a gateway to the composio llm search api, allowing users to perform searches across a broad range of content with multiple filtering options. it accommodates complex queries, including both keyword and phrase searches, with additional parameters to fine-tune the search results. this class enables a tailored search experience by allowing users to specify the search depth, include images and direct answers, apply domain-specific filters, and control the number of results returned. it is designed to meet various search requirements, from quick lookups to in-depth research.",
"inputSchema": {
"type": "object",
"properties": {
"exclude_domains": {
"type": "array",
"items": {
"type": "object",
"properties": {},
"additionalProperties": false
},
"description": "A list of domain names to exclude from the search results. Results from these domains will not be included, which can help to filter out unwanted content."
},
"include_answer": {
"type": "boolean",
"description": "Specifies whether to include direct answers to the query in the search results. Useful for queries that expect a factual answer."
},
"include_domains": {
"type": "array",
"items": {
"type": "object",
"properties": {},
"additionalProperties": false
},
"description": "A list of domain names to include in the search results. Only results from these specified domains will be returned, allowing for targeted searches."
},
"include_images": {
"type": "boolean",
"description": "A flag indicating whether to include images in the search results. When set to true, the response will contain image links related to the query."
},
"include_raw_content": {
"type": "boolean",
"description": "If set to true, the search results will include the raw content from the search index, which may contain unprocessed HTML or text."
},
"max_results": {
"type": "integer",
"description": "The maximum number of search results that the API should return. This limits the size of the result set for the query."
},
"query": {
"type": "string",
"description": "The primary text used to perform the search. This is the key term or phrase that the search functionality will use to retrieve results."
},
"search_depth": {
"type": "string",
"description": "Determines the thoroughness of the search. A 'basic' search might perform a quick and broad search, while 'advanced' could indicate a more intensive and narrow search."
}
},
"required": [
"query"
],
"additionalProperties": false
},
"mcp_info": {
"server_name": "search"
}
},
{
"name": "COMPOSIO_SEARCH_TRENDS_SEARCH",
"description": "The trendssearch class performs a trend search using the google trends search api, to target trend data and information. it uses the google trends search engine through the google trends search api to fetch relevant trend information based on the input query.",
"inputSchema": {
"type": "object",
"properties": {
"data_type": {
"anyOf": [
{
"type": "string"
},
{
"enum": [
"null"
],
"nullable": true
}
],
"description": "Parameter defines the type of search you want to do. Available options: TIMESERIES - Interest over time (default) - Accepts both single and multiple queries per search. GEO_MAP - Compared breakdown by region - Accepts only multiple queries per search. GEO_MAP_0 - Interest by region - Accepts only single query per search. RELATED_TOPICS - Related topics - Accepts only single query per search. RELATED_QUERIES - Related queries - Accepts only single query per search."
},
"query": {
"type": "string",
"description": "The search query for the Google Trends Search API, specifying the trend topic."
}
},
"required": [
"query"
],
"additionalProperties": false
},
"mcp_info": {
"server_name": "search"
}
},
{
"name": "TEXT_TO_PDF_CONVERT_TEXT_TO_PDF",
"description": "Convert text to pdf",
"inputSchema": {
"type": "object",
"properties": {
"file_type": {
"type": "string",
"description": "The type of file to convert to, choose between txt and markdown"
},
"text": {
"type": "string",
"description": "The text to convert to the specified file type"
}
},
"required": [
"file_type",
"text"
],
"additionalProperties": false
},
"mcp_info": {
"server_name": "text_to_pdf"
}
}
]