feat(mcp): add in-memory MCP server support and configuration management (#4359)

This commit is contained in:
LiuVaayne 2025-04-05 14:17:56 +08:00 committed by GitHub
parent 9c6de71fbb
commit ea059d5517
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 138250 additions and 533 deletions

File diff suppressed because one or more lines are too long

View File

@ -70,6 +70,7 @@
"@types/react-infinite-scroll-component": "^5.0.0", "@types/react-infinite-scroll-component": "^5.0.0",
"@xyflow/react": "^12.4.4", "@xyflow/react": "^12.4.4",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"diff": "^7.0.0",
"docx": "^9.0.2", "docx": "^9.0.2",
"electron-log": "^5.1.5", "electron-log": "^5.1.5",
"electron-store": "^8.2.0", "electron-store": "^8.2.0",
@ -79,10 +80,14 @@
"fast-xml-parser": "^5.0.9", "fast-xml-parser": "^5.0.9",
"fetch-socks": "^1.3.2", "fetch-socks": "^1.3.2",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"got-scraping": "^4.1.1",
"jsdom": "^26.0.0",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"officeparser": "^4.1.1", "officeparser": "^4.1.1",
"proxy-agent": "^6.5.0", "proxy-agent": "^6.5.0",
"tar": "^7.4.3", "tar": "^7.4.3",
"turndown": "^7.2.0",
"turndown-plugin-gfm": "^1.0.2",
"undici": "^7.4.0", "undici": "^7.4.0",
"webdav": "^5.8.0", "webdav": "^5.8.0",
"zipread": "^1.3.3" "zipread": "^1.3.3"
@ -109,6 +114,7 @@
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch", "@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
"@tryfabric/martian": "^1.2.4", "@tryfabric/martian": "^1.2.4",
"@types/adm-zip": "^0", "@types/adm-zip": "^0",
"@types/diff": "^7",
"@types/fs-extra": "^11", "@types/fs-extra": "^11",
"@types/lodash": "^4.17.5", "@types/lodash": "^4.17.5",
"@types/markdown-it": "^14", "@types/markdown-it": "^14",

View File

@ -1,6 +1,5 @@
<!doctype html> <!doctype html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@ -43,8 +42,9 @@
<p class="mb-4">如有任何问题或需申请商业授权,请联系 Cherry Studio 开发团队。</p> <p class="mb-4">如有任何问题或需申请商业授权,请联系 Cherry Studio 开发团队。</p>
<p> <p>
除上述特定条件外,其他所有权利和限制均遵循 Apache License 2.0。有关 Apache License 2.0 的详细信息,请访问 除上述特定条件外,其他所有权利和限制均遵循 Apache License 2.0。有关 Apache License 2.0 的详细信息,请访问
<a href="http://www.apache.org/licenses/LICENSE-2.0" <a href="http://www.apache.org/licenses/LICENSE-2.0" class="text-blue-500 underline"
class="text-blue-500 underline">http://www.apache.org/licenses/LICENSE-2.0</a> >http://www.apache.org/licenses/LICENSE-2.0</a
>
</p> </p>
</div> </div>
<h1 class="text-3xl font-bold mb-6 text-center">Cherry Studio License</h1> <h1 class="text-3xl font-bold mb-6 text-center">Cherry Studio License</h1>
@ -57,28 +57,23 @@
<h3 class="text-xl font-semibold mb-2">I. Commercial Use License</h3> <h3 class="text-xl font-semibold mb-2">I. Commercial Use License</h3>
<ol class="list-decimal list-inside mb-4"> <ol class="list-decimal list-inside mb-4">
<li> <li>
<strong>Free Commercial Use</strong>: Users can use the software for commercial purposes without <strong>Free Commercial Use</strong>: Users can use the software for commercial purposes without modifying
modifying
the code. the code.
</li> </li>
<li> <li>
<strong>Commercial License Required</strong>: A commercial license is required if any of the <strong>Commercial License Required</strong>: A commercial license is required if any of the following
following
conditions are met: conditions are met:
<ol class="list-decimal list-inside ml-4"> <ol class="list-decimal list-inside ml-4">
<li> <li>
You modify, develop, or alter the software, including but not limited to changes to the You modify, develop, or alter the software, including but not limited to changes to the application
application
name, logo, code, or functionality. name, logo, code, or functionality.
</li> </li>
<li>You provide multi-tenant services to enterprise customers with 10 or more users.</li> <li>You provide multi-tenant services to enterprise customers with 10 or more users.</li>
<li> <li>
You pre-install or integrate the software into hardware devices or products and bundle it You pre-install or integrate the software into hardware devices or products and bundle it for sale.
for sale.
</li> </li>
<li> <li>
You are engaging in large-scale procurement for government or educational institutions, You are engaging in large-scale procurement for government or educational institutions, especially
especially
involving security, data privacy, or other sensitive requirements. involving security, data privacy, or other sensitive requirements.
</li> </li>
</ol> </ol>
@ -87,13 +82,11 @@
<h3 class="text-xl font-semibold mb-2">II. Contributor Agreement</h3> <h3 class="text-xl font-semibold mb-2">II. Contributor Agreement</h3>
<ol class="list-decimal list-inside mb-4"> <ol class="list-decimal list-inside mb-4">
<li> <li>
<strong>License Adjustment</strong>: The producer reserves the right to adjust the open-source <strong>License Adjustment</strong>: The producer reserves the right to adjust the open-source license as
license as
needed, making it stricter or more lenient. needed, making it stricter or more lenient.
</li> </li>
<li> <li>
<strong>Commercial Use</strong>: Any code you contribute may be used for commercial purposes, <strong>Commercial Use</strong>: Any code you contribute may be used for commercial purposes, including but
including but
not limited to cloud business operations. not limited to cloud business operations.
</li> </li>
</ol> </ol>
@ -108,11 +101,11 @@
<p> <p>
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache
License 2.0. Detailed information about the Apache License 2.0 can be found at License 2.0. Detailed information about the Apache License 2.0 can be found at
<a href="http://www.apache.org/licenses/LICENSE-2.0" <a href="http://www.apache.org/licenses/LICENSE-2.0" class="text-blue-500 underline"
class="text-blue-500 underline">http://www.apache.org/licenses/LICENSE-2.0</a> >http://www.apache.org/licenses/LICENSE-2.0</a
>
</p> </p>
</div> </div>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,6 +1,5 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@ -18,7 +17,8 @@
<!-- Loading状态 --> <!-- Loading状态 -->
<div v-if="loading" class="text-center py-8"> <div v-if="loading" class="text-center py-8">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-4" <div
class="inline-block animate-spin rounded-full h-8 w-8 border-4"
:class="isDark ? 'border-gray-700 border-t-blue-500' : 'border-gray-300 border-t-blue-500'"></div> :class="isDark ? 'border-gray-700 border-t-blue-500' : 'border-gray-300 border-t-blue-500'"></div>
</div> </div>
@ -27,10 +27,14 @@
<!-- Release 列表 --> <!-- Release 列表 -->
<div v-else class="space-y-8"> <div v-else class="space-y-8">
<div v-for="release in releases" :key="release.id" class="relative pl-8" <div
v-for="release in releases"
:key="release.id"
class="relative pl-8"
:class="isDark ? 'border-l-2 border-gray-700' : 'border-l-2 border-gray-200'"> :class="isDark ? 'border-l-2 border-gray-700' : 'border-l-2 border-gray-200'">
<div class="absolute -left-2 top-0 w-4 h-4 rounded-full bg-green-500"></div> <div class="absolute -left-2 top-0 w-4 h-4 rounded-full bg-green-500"></div>
<div class="rounded-lg shadow-sm p-6 transition-shadow" <div
class="rounded-lg shadow-sm p-6 transition-shadow"
:class="isDark ? 'bg-black hover:shadow-md hover:shadow-black' : 'bg-white hover:shadow-md'"> :class="isDark ? 'bg-black hover:shadow-md hover:shadow-black' : 'bg-white hover:shadow-md'">
<div class="flex items-start justify-between mb-4"> <div class="flex items-start justify-between mb-4">
<div> <div>
@ -41,12 +45,15 @@
{{ formatDate(release.published_at) }} {{ formatDate(release.published_at) }}
</p> </p>
</div> </div>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium" <span
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
:class="isDark ? 'bg-green-900 text-green-200' : 'bg-green-100 text-green-800'"> :class="isDark ? 'bg-green-900 text-green-200' : 'bg-green-100 text-green-800'">
{{ release.tag_name }} {{ release.tag_name }}
</span> </span>
</div> </div>
<div class="prose" :class="isDark ? 'text-gray-300 dark-prose' : 'text-gray-600'" <div
class="prose"
:class="isDark ? 'text-gray-300 dark-prose' : 'text-gray-600'"
v-html="renderMarkdown(release.body)"></div> v-html="renderMarkdown(release.body)"></div>
</div> </div>
</div> </div>
@ -198,5 +205,4 @@
} }
</style> </style>
</body> </body>
</html> </html>

View File

@ -1,8 +1,8 @@
declare function decrypt(app: string, s: string): string; declare function decrypt(app: string, s: string): string
interface Secret { interface Secret {
app: string; app: string
} }
declare function createOAuthUrl(secret: Secret): string; declare function createOAuthUrl(secret: Secret): string
export { type Secret, createOAuthUrl, decrypt }; export { type Secret, createOAuthUrl, decrypt }

File diff suppressed because one or more lines are too long

View File

@ -26,7 +26,7 @@ import { TrayService } from './services/TrayService'
import { windowService } from './services/WindowService' import { windowService } from './services/WindowService'
import { getResourcePath } from './utils' import { getResourcePath } from './utils'
import { decrypt, encrypt } from './utils/aes' import { decrypt, encrypt } from './utils/aes'
import { getFilesDir } from './utils/file' import { getConfigDir, getFilesDir } from './utils/file'
import { compress, decompress } from './utils/zip' import { compress, decompress } from './utils/zip'
const fileManager = new FileStorage() const fileManager = new FileStorage()
@ -42,6 +42,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
isPackaged: app.isPackaged, isPackaged: app.isPackaged,
appPath: app.getAppPath(), appPath: app.getAppPath(),
filesPath: getFilesDir(), filesPath: getFilesDir(),
configPath: getConfigDir(),
appDataPath: app.getPath('userData'), appDataPath: app.getPath('userData'),
resourcesPath: getResourcePath(), resourcesPath: getResourcePath(),
logsPath: log.transports.file.getFile().path logsPath: log.transports.file.getFile().path

View File

@ -0,0 +1,374 @@
// Brave Search MCP Server
// port https://github.com/modelcontextprotocol/servers/blob/main/src/brave-search/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from '@modelcontextprotocol/sdk/types.js'
const WEB_SEARCH_TOOL: Tool = {
name: 'brave_web_search',
description:
'Performs a web search using the Brave Search API, ideal for general queries, news, articles, and online content. ' +
'Use this for broad information gathering, recent events, or when you need diverse web sources. ' +
'Supports pagination, content filtering, and freshness controls. ' +
'Maximum 20 results per request, with offset for pagination. ',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query (max 400 chars, 50 words)'
},
count: {
type: 'number',
description: 'Number of results (1-20, default 10)',
default: 10
},
offset: {
type: 'number',
description: 'Pagination offset (max 9, default 0)',
default: 0
}
},
required: ['query']
}
}
const LOCAL_SEARCH_TOOL: Tool = {
name: 'brave_local_search',
description:
"Searches for local businesses and places using Brave's Local Search API. " +
'Best for queries related to physical locations, businesses, restaurants, services, etc. ' +
'Returns detailed information including:\n' +
'- Business names and addresses\n' +
'- Ratings and review counts\n' +
'- Phone numbers and opening hours\n' +
"Use this when the query implies 'near me' or mentions specific locations. " +
'Automatically falls back to web search if no local results are found.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: "Local search query (e.g. 'pizza near Central Park')"
},
count: {
type: 'number',
description: 'Number of results (1-20, default 5)',
default: 5
}
},
required: ['query']
}
}
const RATE_LIMIT = {
perSecond: 1,
perMonth: 15000
}
const requestCount = {
second: 0,
month: 0,
lastReset: Date.now()
}
function checkRateLimit() {
const now = Date.now()
if (now - requestCount.lastReset > 1000) {
requestCount.second = 0
requestCount.lastReset = now
}
if (requestCount.second >= RATE_LIMIT.perSecond || requestCount.month >= RATE_LIMIT.perMonth) {
throw new Error('Rate limit exceeded')
}
requestCount.second++
requestCount.month++
}
interface BraveWeb {
web?: {
results?: Array<{
title: string
description: string
url: string
language?: string
published?: string
rank?: number
}>
}
locations?: {
results?: Array<{
id: string // Required by API
title?: string
}>
}
}
interface BraveLocation {
id: string
name: string
address: {
streetAddress?: string
addressLocality?: string
addressRegion?: string
postalCode?: string
}
coordinates?: {
latitude: number
longitude: number
}
phone?: string
rating?: {
ratingValue?: number
ratingCount?: number
}
openingHours?: string[]
priceRange?: string
}
interface BravePoiResponse {
results: BraveLocation[]
}
interface BraveDescription {
descriptions: { [id: string]: string }
}
function isBraveWebSearchArgs(args: unknown): args is { query: string; count?: number } {
return (
typeof args === 'object' &&
args !== null &&
'query' in args &&
typeof (args as { query: string }).query === 'string'
)
}
function isBraveLocalSearchArgs(args: unknown): args is { query: string; count?: number } {
return (
typeof args === 'object' &&
args !== null &&
'query' in args &&
typeof (args as { query: string }).query === 'string'
)
}
async function performWebSearch(apiKey: string, query: string, count: number = 10, offset: number = 0) {
checkRateLimit()
const url = new URL('https://api.search.brave.com/res/v1/web/search')
url.searchParams.set('q', query)
url.searchParams.set('count', Math.min(count, 20).toString()) // API limit
url.searchParams.set('offset', offset.toString())
const response = await fetch(url, {
headers: {
Accept: 'application/json',
'Accept-Encoding': 'gzip',
'X-Subscription-Token': apiKey
}
})
if (!response.ok) {
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`)
}
const data = (await response.json()) as BraveWeb
// Extract just web results
const results = (data.web?.results || []).map((result) => ({
title: result.title || '',
description: result.description || '',
url: result.url || ''
}))
return results.map((r) => `Title: ${r.title}\nDescription: ${r.description}\nURL: ${r.url}`).join('\n\n')
}
async function performLocalSearch(apiKey: string, query: string, count: number = 5) {
checkRateLimit()
// Initial search to get location IDs
const webUrl = new URL('https://api.search.brave.com/res/v1/web/search')
webUrl.searchParams.set('q', query)
webUrl.searchParams.set('search_lang', 'en')
webUrl.searchParams.set('result_filter', 'locations')
webUrl.searchParams.set('count', Math.min(count, 20).toString())
const webResponse = await fetch(webUrl, {
headers: {
Accept: 'application/json',
'Accept-Encoding': 'gzip',
'X-Subscription-Token': apiKey
}
})
if (!webResponse.ok) {
throw new Error(`Brave API error: ${webResponse.status} ${webResponse.statusText}\n${await webResponse.text()}`)
}
const webData = (await webResponse.json()) as BraveWeb
const locationIds =
webData.locations?.results?.filter((r): r is { id: string; title?: string } => r.id != null).map((r) => r.id) || []
if (locationIds.length === 0) {
return performWebSearch(apiKey, query, count) // Fallback to web search
}
// Get POI details and descriptions in parallel
const [poisData, descriptionsData] = await Promise.all([
getPoisData(apiKey, locationIds),
getDescriptionsData(apiKey, locationIds)
])
return formatLocalResults(poisData, descriptionsData)
}
async function getPoisData(apiKey: string, ids: string[]): Promise<BravePoiResponse> {
checkRateLimit()
const url = new URL('https://api.search.brave.com/res/v1/local/pois')
ids.filter(Boolean).forEach((id) => url.searchParams.append('ids', id))
const response = await fetch(url, {
headers: {
Accept: 'application/json',
'Accept-Encoding': 'gzip',
'X-Subscription-Token': apiKey
}
})
if (!response.ok) {
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`)
}
const poisResponse = (await response.json()) as BravePoiResponse
return poisResponse
}
async function getDescriptionsData(apiKey: string, ids: string[]): Promise<BraveDescription> {
checkRateLimit()
const url = new URL('https://api.search.brave.com/res/v1/local/descriptions')
ids.filter(Boolean).forEach((id) => url.searchParams.append('ids', id))
const response = await fetch(url, {
headers: {
Accept: 'application/json',
'Accept-Encoding': 'gzip',
'X-Subscription-Token': apiKey
}
})
if (!response.ok) {
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`)
}
const descriptionsData = (await response.json()) as BraveDescription
return descriptionsData
}
function formatLocalResults(poisData: BravePoiResponse, descData: BraveDescription): string {
return (
(poisData.results || [])
.map((poi) => {
const address =
[
poi.address?.streetAddress ?? '',
poi.address?.addressLocality ?? '',
poi.address?.addressRegion ?? '',
poi.address?.postalCode ?? ''
]
.filter((part) => part !== '')
.join(', ') || 'N/A'
return `Name: ${poi.name}
Address: ${address}
Phone: ${poi.phone || 'N/A'}
Rating: ${poi.rating?.ratingValue ?? 'N/A'} (${poi.rating?.ratingCount ?? 0} reviews)
Price Range: ${poi.priceRange || 'N/A'}
Hours: ${(poi.openingHours || []).join(', ') || 'N/A'}
Description: ${descData.descriptions[poi.id] || 'No description available'}
`
})
.join('\n---\n') || 'No local results found'
)
}
class BraveSearchServer {
public server: Server
private apiKey: string
constructor(apiKey: string) {
if (!apiKey) {
throw new Error('BRAVE_API_KEY is required for Brave Search MCP server')
}
this.apiKey = apiKey
this.server = new Server(
{
name: 'brave-search-server',
version: '0.1.0'
},
{
capabilities: {
tools: {}
}
}
)
this.initialize()
}
initialize() {
// Tool handlers
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [WEB_SEARCH_TOOL, LOCAL_SEARCH_TOOL]
}))
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params
if (!args) {
throw new Error('No arguments provided')
}
switch (name) {
case 'brave_web_search': {
if (!isBraveWebSearchArgs(args)) {
throw new Error('Invalid arguments for brave_web_search')
}
const { query, count = 10 } = args
const results = await performWebSearch(this.apiKey, query, count)
return {
content: [{ type: 'text', text: results }],
isError: false
}
}
case 'brave_local_search': {
if (!isBraveLocalSearchArgs(args)) {
throw new Error('Invalid arguments for brave_local_search')
}
const { query, count = 5 } = args
const results = await performLocalSearch(this.apiKey, query, count)
return {
content: [{ type: 'text', text: results }],
isError: false
}
}
default:
return {
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
isError: true
}
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
}
}
})
}
}
export default BraveSearchServer

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,36 @@
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import Logger from 'electron-log'
import BraveSearchServer from './brave-search'
import EverythingServer from './everything'
import FetchServer from './fetch'
import FileSystemServer from './filesystem'
import MemoryServer from './memory'
import ThinkingServer from './sequentialthinking'
export function createInMemoryMCPServer(name: string, args: string[] = [], envs: Record<string, string> = {}): Server {
Logger.info(`[MCP] Creating in-memory MCP server: ${name} with args: ${args} and envs: ${JSON.stringify(envs)}`)
switch (name) {
case '@cherry/memory': {
const envPath = envs.MEMORY_FILE_PATH
return new MemoryServer(envPath).server
}
case '@cherry/sequentialthinking': {
return new ThinkingServer().server
}
case '@cherry/brave-search': {
return new BraveSearchServer(envs.BRAVE_API_KEY).server
}
case '@cherry/everything': {
return new EverythingServer().server
}
case '@cherry/fetch': {
return new FetchServer().server
}
case '@cherry/filesystem': {
return new FileSystemServer(args).server
}
default:
throw new Error(`Unknown in-memory MCP server: ${name}`)
}
}

View File

@ -0,0 +1,236 @@
// port https://github.com/zcaceres/fetch-mcp/blob/main/src/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
import { JSDOM } from 'jsdom'
import TurndownService from 'turndown'
import { z } from 'zod'
export const RequestPayloadSchema = z.object({
url: z.string().url(),
headers: z.record(z.string()).optional()
})
export type RequestPayload = z.infer<typeof RequestPayloadSchema>
export class Fetcher {
private static async _fetch({ url, headers }: RequestPayload): Promise<Response> {
try {
const response = await fetch(url, {
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
...headers
}
})
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`)
}
return response
} catch (e: unknown) {
if (e instanceof Error) {
throw new Error(`Failed to fetch ${url}: ${e.message}`)
} else {
throw new Error(`Failed to fetch ${url}: Unknown error`)
}
}
}
static async html(requestPayload: RequestPayload) {
try {
const response = await this._fetch(requestPayload)
const html = await response.text()
return { content: [{ type: 'text', text: html }], isError: false }
} catch (error) {
return {
content: [{ type: 'text', text: (error as Error).message }],
isError: true
}
}
}
static async json(requestPayload: RequestPayload) {
try {
const response = await this._fetch(requestPayload)
const json = await response.json()
return {
content: [{ type: 'text', text: JSON.stringify(json) }],
isError: false
}
} catch (error) {
return {
content: [{ type: 'text', text: (error as Error).message }],
isError: true
}
}
}
static async txt(requestPayload: RequestPayload) {
try {
const response = await this._fetch(requestPayload)
const html = await response.text()
const dom = new JSDOM(html)
const document = dom.window.document
const scripts = document.getElementsByTagName('script')
const styles = document.getElementsByTagName('style')
Array.from(scripts).forEach((script: any) => script.remove())
Array.from(styles).forEach((style: any) => style.remove())
const text = document.body.textContent || ''
const normalizedText = text.replace(/\s+/g, ' ').trim()
return {
content: [{ type: 'text', text: normalizedText }],
isError: false
}
} catch (error) {
return {
content: [{ type: 'text', text: (error as Error).message }],
isError: true
}
}
}
static async markdown(requestPayload: RequestPayload) {
try {
const response = await this._fetch(requestPayload)
const html = await response.text()
const turndownService = new TurndownService()
const markdown = turndownService.turndown(html)
return { content: [{ type: 'text', text: markdown }], isError: false }
} catch (error) {
return {
content: [{ type: 'text', text: (error as Error).message }],
isError: true
}
}
}
}
const server = new Server(
{
name: 'zcaceres/fetch',
version: '0.1.0'
},
{
capabilities: {
resources: {},
tools: {}
}
}
)
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'fetch_html',
description: 'Fetch a website and return the content as HTML',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL of the website to fetch'
},
headers: {
type: 'object',
description: 'Optional headers to include in the request'
}
},
required: ['url']
}
},
{
name: 'fetch_markdown',
description: 'Fetch a website and return the content as Markdown',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL of the website to fetch'
},
headers: {
type: 'object',
description: 'Optional headers to include in the request'
}
},
required: ['url']
}
},
{
name: 'fetch_txt',
description: 'Fetch a website, return the content as plain text (no HTML)',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL of the website to fetch'
},
headers: {
type: 'object',
description: 'Optional headers to include in the request'
}
},
required: ['url']
}
},
{
name: 'fetch_json',
description: 'Fetch a JSON file from a URL',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL of the JSON to fetch'
},
headers: {
type: 'object',
description: 'Optional headers to include in the request'
}
},
required: ['url']
}
}
]
}
})
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { arguments: args } = request.params
const validatedArgs = RequestPayloadSchema.parse(args)
if (request.params.name === 'fetch_html') {
const fetchResult = await Fetcher.html(validatedArgs)
return fetchResult
}
if (request.params.name === 'fetch_json') {
const fetchResult = await Fetcher.json(validatedArgs)
return fetchResult
}
if (request.params.name === 'fetch_txt') {
const fetchResult = await Fetcher.txt(validatedArgs)
return fetchResult
}
if (request.params.name === 'fetch_markdown') {
const fetchResult = await Fetcher.markdown(validatedArgs)
return fetchResult
}
throw new Error('Tool not found')
})
class FetchServer {
public server: Server
constructor() {
this.server = server
}
}
export default FetchServer

View File

@ -0,0 +1,655 @@
// port https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema } from '@modelcontextprotocol/sdk/types.js'
import { createTwoFilesPatch } from 'diff'
import fs from 'fs/promises'
import { minimatch } from 'minimatch'
import os from 'os'
import path from 'path'
import { z } from 'zod'
import { zodToJsonSchema } from 'zod-to-json-schema'
// Normalize all paths consistently
function normalizePath(p: string): string {
return path.normalize(p)
}
function expandHome(filepath: string): string {
if (filepath.startsWith('~/') || filepath === '~') {
return path.join(os.homedir(), filepath.slice(1))
}
return filepath
}
// Security utilities
async function validatePath(allowedDirectories: string[], requestedPath: string): Promise<string> {
const expandedPath = expandHome(requestedPath)
const absolute = path.isAbsolute(expandedPath)
? path.resolve(expandedPath)
: path.resolve(process.cwd(), expandedPath)
const normalizedRequested = normalizePath(absolute)
// Check if path is within allowed directories
const isAllowed = allowedDirectories.some((dir) => normalizedRequested.startsWith(dir))
if (!isAllowed) {
throw new Error(
`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(', ')}`
)
}
// Handle symlinks by checking their real path
try {
const realPath = await fs.realpath(absolute)
const normalizedReal = normalizePath(realPath)
const isRealPathAllowed = allowedDirectories.some((dir) => normalizedReal.startsWith(dir))
if (!isRealPathAllowed) {
throw new Error('Access denied - symlink target outside allowed directories')
}
return realPath
} catch (error) {
// For new files that don't exist yet, verify parent directory
const parentDir = path.dirname(absolute)
try {
const realParentPath = await fs.realpath(parentDir)
const normalizedParent = normalizePath(realParentPath)
const isParentAllowed = allowedDirectories.some((dir) => normalizedParent.startsWith(dir))
if (!isParentAllowed) {
throw new Error('Access denied - parent directory outside allowed directories')
}
return absolute
} catch {
throw new Error(`Parent directory does not exist: ${parentDir}`)
}
}
}
// Schema definitions
const ReadFileArgsSchema = z.object({
path: z.string()
})
const ReadMultipleFilesArgsSchema = z.object({
paths: z.array(z.string())
})
const WriteFileArgsSchema = z.object({
path: z.string(),
content: z.string()
})
const EditOperation = z.object({
oldText: z.string().describe('Text to search for - must match exactly'),
newText: z.string().describe('Text to replace with')
})
const EditFileArgsSchema = z.object({
path: z.string(),
edits: z.array(EditOperation),
dryRun: z.boolean().default(false).describe('Preview changes using git-style diff format')
})
const CreateDirectoryArgsSchema = z.object({
path: z.string()
})
const ListDirectoryArgsSchema = z.object({
path: z.string()
})
const DirectoryTreeArgsSchema = z.object({
path: z.string()
})
const MoveFileArgsSchema = z.object({
source: z.string(),
destination: z.string()
})
const SearchFilesArgsSchema = z.object({
path: z.string(),
pattern: z.string(),
excludePatterns: z.array(z.string()).optional().default([])
})
const GetFileInfoArgsSchema = z.object({
path: z.string()
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ToolInputSchema = ToolSchema.shape.inputSchema
type ToolInput = z.infer<typeof ToolInputSchema>
interface FileInfo {
size: number
created: Date
modified: Date
accessed: Date
isDirectory: boolean
isFile: boolean
permissions: string
}
// Tool implementations
async function getFileStats(filePath: string): Promise<FileInfo> {
const stats = await fs.stat(filePath)
return {
size: stats.size,
created: stats.birthtime,
modified: stats.mtime,
accessed: stats.atime,
isDirectory: stats.isDirectory(),
isFile: stats.isFile(),
permissions: stats.mode.toString(8).slice(-3)
}
}
async function searchFiles(
allowedDirectories: string[],
rootPath: string,
pattern: string,
excludePatterns: string[] = []
): Promise<string[]> {
const results: string[] = []
async function search(currentPath: string) {
const entries = await fs.readdir(currentPath, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name)
try {
// Validate each path before processing
await validatePath(allowedDirectories, fullPath)
// Check if path matches any exclude pattern
const relativePath = path.relative(rootPath, fullPath)
const shouldExclude = excludePatterns.some((pattern) => {
const globPattern = pattern.includes('*') ? pattern : `**/${pattern}/**`
return minimatch(relativePath, globPattern, { dot: true })
})
if (shouldExclude) {
continue
}
if (entry.name.toLowerCase().includes(pattern.toLowerCase())) {
results.push(fullPath)
}
if (entry.isDirectory()) {
await search(fullPath)
}
} catch (error) {
// Skip invalid paths during search
continue
}
}
}
await search(rootPath)
return results
}
// file editing and diffing utilities
function normalizeLineEndings(text: string): string {
return text.replace(/\r\n/g, '\n')
}
function createUnifiedDiff(originalContent: string, newContent: string, filepath: string = 'file'): string {
// Ensure consistent line endings for diff
const normalizedOriginal = normalizeLineEndings(originalContent)
const normalizedNew = normalizeLineEndings(newContent)
return createTwoFilesPatch(filepath, filepath, normalizedOriginal, normalizedNew, 'original', 'modified')
}
async function applyFileEdits(
filePath: string,
edits: Array<{ oldText: string; newText: string }>,
dryRun = false
): Promise<string> {
// Read file content and normalize line endings
const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8'))
// Apply edits sequentially
let modifiedContent = content
for (const edit of edits) {
const normalizedOld = normalizeLineEndings(edit.oldText)
const normalizedNew = normalizeLineEndings(edit.newText)
// If exact match exists, use it
if (modifiedContent.includes(normalizedOld)) {
modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew)
continue
}
// Otherwise, try line-by-line matching with flexibility for whitespace
const oldLines = normalizedOld.split('\n')
const contentLines = modifiedContent.split('\n')
let matchFound = false
for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
const potentialMatch = contentLines.slice(i, i + oldLines.length)
// Compare lines with normalized whitespace
const isMatch = oldLines.every((oldLine, j) => {
const contentLine = potentialMatch[j]
return oldLine.trim() === contentLine.trim()
})
if (isMatch) {
// Preserve original indentation of first line
const originalIndent = contentLines[i].match(/^\s*/)?.[0] || ''
const newLines = normalizedNew.split('\n').map((line, j) => {
if (j === 0) return originalIndent + line.trimStart()
// For subsequent lines, try to preserve relative indentation
const oldIndent = oldLines[j]?.match(/^\s*/)?.[0] || ''
const newIndent = line.match(/^\s*/)?.[0] || ''
if (oldIndent && newIndent) {
const relativeIndent = newIndent.length - oldIndent.length
return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart()
}
return line
})
contentLines.splice(i, oldLines.length, ...newLines)
modifiedContent = contentLines.join('\n')
matchFound = true
break
}
}
if (!matchFound) {
throw new Error(`Could not find exact match for edit:\n${edit.oldText}`)
}
}
// Create unified diff
const diff = createUnifiedDiff(content, modifiedContent, filePath)
// Format diff with appropriate number of backticks
let numBackticks = 3
while (diff.includes('`'.repeat(numBackticks))) {
numBackticks++
}
const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`
if (!dryRun) {
await fs.writeFile(filePath, modifiedContent, 'utf-8')
}
return formattedDiff
}
class FileSystemServer {
public server: Server
private allowedDirectories: string[]
constructor(allowedDirs: string[]) {
if (!Array.isArray(allowedDirs) || allowedDirs.length === 0) {
throw new Error('No allowed directories provided, please specify at least one directory in args')
}
this.allowedDirectories = allowedDirs.map((dir) => normalizePath(path.resolve(expandHome(dir))))
// Validate that all directories exist and are accessible
this.validateDirs().catch((error) => {
console.error('Error validating allowed directories:', error)
process.exit(1)
})
this.server = new Server(
{
name: 'secure-filesystem-server',
version: '0.2.0'
},
{
capabilities: {
tools: {}
}
}
)
this.initialize()
}
async validateDirs() {
// Validate that all directories exist and are accessible
await Promise.all(
this.allowedDirectories.map(async (dir) => {
try {
const stats = await fs.stat(expandHome(dir))
if (!stats.isDirectory()) {
console.error(`Error: ${dir} is not a directory`)
process.exit(1)
}
} catch (error) {
console.error(`Error accessing directory ${dir}:`, error)
process.exit(1)
}
})
)
}
initialize() {
// Tool handlers
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'read_file',
description:
'Read the complete contents of a file from the file system. ' +
'Handles various text encodings and provides detailed error messages ' +
'if the file cannot be read. Use this tool when you need to examine ' +
'the contents of a single file. Only works within allowed directories.',
inputSchema: zodToJsonSchema(ReadFileArgsSchema) as ToolInput
},
{
name: 'read_multiple_files',
description:
'Read the contents of multiple files simultaneously. This is more ' +
'efficient than reading files one by one when you need to analyze ' +
"or compare multiple files. Each file's content is returned with its " +
"path as a reference. Failed reads for individual files won't stop " +
'the entire operation. Only works within allowed directories.',
inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema) as ToolInput
},
{
name: 'write_file',
description:
'Create a new file or completely overwrite an existing file with new content. ' +
'Use with caution as it will overwrite existing files without warning. ' +
'Handles text content with proper encoding. Only works within allowed directories.',
inputSchema: zodToJsonSchema(WriteFileArgsSchema) as ToolInput
},
{
name: 'edit_file',
description:
'Make line-based edits to a text file. Each edit replaces exact line sequences ' +
'with new content. Returns a git-style diff showing the changes made. ' +
'Only works within allowed directories.',
inputSchema: zodToJsonSchema(EditFileArgsSchema) as ToolInput
},
{
name: 'create_directory',
description:
'Create a new directory or ensure a directory exists. Can create multiple ' +
'nested directories in one operation. If the directory already exists, ' +
'this operation will succeed silently. Perfect for setting up directory ' +
'structures for projects or ensuring required paths exist. Only works within allowed directories.',
inputSchema: zodToJsonSchema(CreateDirectoryArgsSchema) as ToolInput
},
{
name: 'list_directory',
description:
'Get a detailed listing of all files and directories in a specified path. ' +
'Results clearly distinguish between files and directories with [FILE] and [DIR] ' +
'prefixes. This tool is essential for understanding directory structure and ' +
'finding specific files within a directory. Only works within allowed directories.',
inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput
},
{
name: 'directory_tree',
description:
'Get a recursive tree view of files and directories as a JSON structure. ' +
"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
'Files have no children array, while directories always have a children array (which may be empty). ' +
'The output is formatted with 2-space indentation for readability. Only works within allowed directories.',
inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput
},
{
name: 'move_file',
description:
'Move or rename files and directories. Can move files between directories ' +
'and rename them in a single operation. If the destination exists, the ' +
'operation will fail. Works across different directories and can be used ' +
'for simple renaming within the same directory. Both source and destination must be within allowed directories.',
inputSchema: zodToJsonSchema(MoveFileArgsSchema) as ToolInput
},
{
name: 'search_files',
description:
'Recursively search for files and directories matching a pattern. ' +
'Searches through all subdirectories from the starting path. The search ' +
'is case-insensitive and matches partial names. Returns full paths to all ' +
"matching items. Great for finding files when you don't know their exact location. " +
'Only searches within allowed directories.',
inputSchema: zodToJsonSchema(SearchFilesArgsSchema) as ToolInput
},
{
name: 'get_file_info',
description:
'Retrieve detailed metadata about a file or directory. Returns comprehensive ' +
'information including size, creation time, last modified time, permissions, ' +
'and type. This tool is perfect for understanding file characteristics ' +
'without reading the actual content. Only works within allowed directories.',
inputSchema: zodToJsonSchema(GetFileInfoArgsSchema) as ToolInput
},
{
name: 'list_allowed_directories',
description:
'Returns the list of directories that this server is allowed to access. ' +
'Use this to understand which directories are available before trying to access files.',
inputSchema: {
type: 'object',
properties: {},
required: []
}
}
]
}
})
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params
switch (name) {
case 'read_file': {
const parsed = ReadFileArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`Invalid arguments for read_file: ${parsed.error}`)
}
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
const content = await fs.readFile(validPath, 'utf-8')
return {
content: [{ type: 'text', text: content }]
}
}
case 'read_multiple_files': {
const parsed = ReadMultipleFilesArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`Invalid arguments for read_multiple_files: ${parsed.error}`)
}
const results = await Promise.all(
parsed.data.paths.map(async (filePath: string) => {
try {
const validPath = await validatePath(this.allowedDirectories, filePath)
const content = await fs.readFile(validPath, 'utf-8')
return `${filePath}:\n${content}\n`
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
return `${filePath}: Error - ${errorMessage}`
}
})
)
return {
content: [{ type: 'text', text: results.join('\n---\n') }]
}
}
case 'write_file': {
const parsed = WriteFileArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`Invalid arguments for write_file: ${parsed.error}`)
}
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
await fs.writeFile(validPath, parsed.data.content, 'utf-8')
return {
content: [{ type: 'text', text: `Successfully wrote to ${parsed.data.path}` }]
}
}
case 'edit_file': {
const parsed = EditFileArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`Invalid arguments for edit_file: ${parsed.error}`)
}
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
const result = await applyFileEdits(validPath, parsed.data.edits, parsed.data.dryRun)
return {
content: [{ type: 'text', text: result }]
}
}
case 'create_directory': {
const parsed = CreateDirectoryArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`Invalid arguments for create_directory: ${parsed.error}`)
}
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
await fs.mkdir(validPath, { recursive: true })
return {
content: [{ type: 'text', text: `Successfully created directory ${parsed.data.path}` }]
}
}
case 'list_directory': {
const parsed = ListDirectoryArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`Invalid arguments for list_directory: ${parsed.error}`)
}
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
const entries = await fs.readdir(validPath, { withFileTypes: true })
const formatted = entries
.map((entry) => `${entry.isDirectory() ? '[DIR]' : '[FILE]'} ${entry.name}`)
.join('\n')
return {
content: [{ type: 'text', text: formatted }]
}
}
case 'directory_tree': {
const parsed = DirectoryTreeArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`)
}
interface TreeEntry {
name: string
type: 'file' | 'directory'
children?: TreeEntry[]
}
async function buildTree(allowedDirectories: string[], currentPath: string): Promise<TreeEntry[]> {
const validPath = await validatePath(allowedDirectories, currentPath)
const entries = await fs.readdir(validPath, { withFileTypes: true })
const result: TreeEntry[] = []
for (const entry of entries) {
const entryData: TreeEntry = {
name: entry.name,
type: entry.isDirectory() ? 'directory' : 'file'
}
if (entry.isDirectory()) {
const subPath = path.join(currentPath, entry.name)
entryData.children = await buildTree(allowedDirectories, subPath)
}
result.push(entryData)
}
return result
}
const treeData = await buildTree(this.allowedDirectories, parsed.data.path)
return {
content: [
{
type: 'text',
text: JSON.stringify(treeData, null, 2)
}
]
}
}
case 'move_file': {
const parsed = MoveFileArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`Invalid arguments for move_file: ${parsed.error}`)
}
const validSourcePath = await validatePath(this.allowedDirectories, parsed.data.source)
const validDestPath = await validatePath(this.allowedDirectories, parsed.data.destination)
await fs.rename(validSourcePath, validDestPath)
return {
content: [
{ type: 'text', text: `Successfully moved ${parsed.data.source} to ${parsed.data.destination}` }
]
}
}
case 'search_files': {
const parsed = SearchFilesArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`Invalid arguments for search_files: ${parsed.error}`)
}
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
const results = await searchFiles(
this.allowedDirectories,
validPath,
parsed.data.pattern,
parsed.data.excludePatterns
)
return {
content: [{ type: 'text', text: results.length > 0 ? results.join('\n') : 'No matches found' }]
}
}
case 'get_file_info': {
const parsed = GetFileInfoArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`Invalid arguments for get_file_info: ${parsed.error}`)
}
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
const info = await getFileStats(validPath)
return {
content: [
{
type: 'text',
text: Object.entries(info)
.map(([key, value]) => `${key}: ${value}`)
.join('\n')
}
]
}
}
case 'list_allowed_directories': {
return {
content: [
{
type: 'text',
text: `Allowed directories:\n${this.allowedDirectories.join('\n')}`
}
]
}
}
default:
throw new Error(`Unknown tool: ${name}`)
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
return {
content: [{ type: 'text', text: `Error: ${errorMessage}` }],
isError: true
}
}
})
}
}
export default FileSystemServer

View File

@ -0,0 +1,490 @@
// port https://github.com/modelcontextprotocol/servers/blob/main/src/memory/index.ts
import { getConfigDir } from '@main/utils/file'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
import { promises as fs } from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
// Define memory file path using environment variable with fallback
const defaultMemoryPath = path.join(getConfigDir(), 'memory.json')
// We are storing our memory using entities, relations, and observations in a graph structure
interface Entity {
name: string
entityType: string
observations: string[]
}
interface Relation {
from: string
to: string
relationType: string
}
interface KnowledgeGraph {
entities: Entity[]
relations: Relation[]
}
// The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
class KnowledgeGraphManager {
private memoryPath: string
constructor(memoryPath: string) {
this.memoryPath = memoryPath
}
private async loadGraph(): Promise<KnowledgeGraph> {
try {
const data = await fs.readFile(this.memoryPath, 'utf-8')
const lines = data.split('\n').filter((line) => line.trim() !== '')
return lines.reduce(
(graph: KnowledgeGraph, line) => {
const item = JSON.parse(line)
if (item.type === 'entity') graph.entities.push(item as Entity)
if (item.type === 'relation') graph.relations.push(item as Relation)
return graph
},
{ entities: [], relations: [] }
)
} catch (error) {
if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') {
return { entities: [], relations: [] }
}
throw error
}
}
private async saveGraph(graph: KnowledgeGraph): Promise<void> {
const lines = [
...graph.entities.map((e) => JSON.stringify({ type: 'entity', ...e })),
...graph.relations.map((r) => JSON.stringify({ type: 'relation', ...r }))
]
await fs.writeFile(this.memoryPath, lines.join('\n'))
}
async createEntities(entities: Entity[]): Promise<Entity[]> {
const graph = await this.loadGraph()
const newEntities = entities.filter((e) => !graph.entities.some((existingEntity) => existingEntity.name === e.name))
graph.entities.push(...newEntities)
await this.saveGraph(graph)
return newEntities
}
async createRelations(relations: Relation[]): Promise<Relation[]> {
const graph = await this.loadGraph()
const newRelations = relations.filter(
(r) =>
!graph.relations.some(
(existingRelation) =>
existingRelation.from === r.from &&
existingRelation.to === r.to &&
existingRelation.relationType === r.relationType
)
)
graph.relations.push(...newRelations)
await this.saveGraph(graph)
return newRelations
}
async addObservations(
observations: { entityName: string; contents: string[] }[]
): Promise<{ entityName: string; addedObservations: string[] }[]> {
const graph = await this.loadGraph()
const results = observations.map((o) => {
const entity = graph.entities.find((e) => e.name === o.entityName)
if (!entity) {
throw new Error(`Entity with name ${o.entityName} not found`)
}
const newObservations = o.contents.filter((content) => !entity.observations.includes(content))
entity.observations.push(...newObservations)
return { entityName: o.entityName, addedObservations: newObservations }
})
await this.saveGraph(graph)
return results
}
async deleteEntities(entityNames: string[]): Promise<void> {
const graph = await this.loadGraph()
graph.entities = graph.entities.filter((e) => !entityNames.includes(e.name))
graph.relations = graph.relations.filter((r) => !entityNames.includes(r.from) && !entityNames.includes(r.to))
await this.saveGraph(graph)
}
async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise<void> {
const graph = await this.loadGraph()
deletions.forEach((d) => {
const entity = graph.entities.find((e) => e.name === d.entityName)
if (entity) {
entity.observations = entity.observations.filter((o) => !d.observations.includes(o))
}
})
await this.saveGraph(graph)
}
async deleteRelations(relations: Relation[]): Promise<void> {
const graph = await this.loadGraph()
graph.relations = graph.relations.filter(
(r) =>
!relations.some(
(delRelation) =>
r.from === delRelation.from && r.to === delRelation.to && r.relationType === delRelation.relationType
)
)
await this.saveGraph(graph)
}
async readGraph(): Promise<KnowledgeGraph> {
return this.loadGraph()
}
// Very basic search function
async searchNodes(query: string): Promise<KnowledgeGraph> {
const graph = await this.loadGraph()
// Filter entities
const filteredEntities = graph.entities.filter(
(e) =>
e.name.toLowerCase().includes(query.toLowerCase()) ||
e.entityType.toLowerCase().includes(query.toLowerCase()) ||
e.observations.some((o) => o.toLowerCase().includes(query.toLowerCase()))
)
// Create a Set of filtered entity names for quick lookup
const filteredEntityNames = new Set(filteredEntities.map((e) => e.name))
// Filter relations to only include those between filtered entities
const filteredRelations = graph.relations.filter(
(r) => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)
)
const filteredGraph: KnowledgeGraph = {
entities: filteredEntities,
relations: filteredRelations
}
return filteredGraph
}
async openNodes(names: string[]): Promise<KnowledgeGraph> {
const graph = await this.loadGraph()
// Filter entities
const filteredEntities = graph.entities.filter((e) => names.includes(e.name))
// Create a Set of filtered entity names for quick lookup
const filteredEntityNames = new Set(filteredEntities.map((e) => e.name))
// Filter relations to only include those between filtered entities
const filteredRelations = graph.relations.filter(
(r) => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)
)
const filteredGraph: KnowledgeGraph = {
entities: filteredEntities,
relations: filteredRelations
}
return filteredGraph
}
}
class MemoryServer {
public server: Server
private knowledgeGraphManager: KnowledgeGraphManager
constructor(envPath: string = '') {
const memoryPath = envPath
? path.isAbsolute(envPath)
? envPath
: path.join(path.dirname(fileURLToPath(import.meta.url)), envPath)
: defaultMemoryPath
this.knowledgeGraphManager = new KnowledgeGraphManager(memoryPath)
this.server = new Server(
{
name: 'memory-server',
version: '1.0.0'
},
{
capabilities: {
tools: {}
}
}
)
this.initialize()
}
initialize() {
// The server instance and tools exposed to Claude
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'create_entities',
description: 'Create multiple new entities in the knowledge graph',
inputSchema: {
type: 'object',
properties: {
entities: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'The name of the entity' },
entityType: { type: 'string', description: 'The type of the entity' },
observations: {
type: 'array',
items: { type: 'string' },
description: 'An array of observation contents associated with the entity'
}
},
required: ['name', 'entityType', 'observations']
}
}
},
required: ['entities']
}
},
{
name: 'create_relations',
description:
'Create multiple new relations between entities in the knowledge graph. Relations should be in active voice',
inputSchema: {
type: 'object',
properties: {
relations: {
type: 'array',
items: {
type: 'object',
properties: {
from: { type: 'string', description: 'The name of the entity where the relation starts' },
to: { type: 'string', description: 'The name of the entity where the relation ends' },
relationType: { type: 'string', description: 'The type of the relation' }
},
required: ['from', 'to', 'relationType']
}
}
},
required: ['relations']
}
},
{
name: 'add_observations',
description: 'Add new observations to existing entities in the knowledge graph',
inputSchema: {
type: 'object',
properties: {
observations: {
type: 'array',
items: {
type: 'object',
properties: {
entityName: { type: 'string', description: 'The name of the entity to add the observations to' },
contents: {
type: 'array',
items: { type: 'string' },
description: 'An array of observation contents to add'
}
},
required: ['entityName', 'contents']
}
}
},
required: ['observations']
}
},
{
name: 'delete_entities',
description: 'Delete multiple entities and their associated relations from the knowledge graph',
inputSchema: {
type: 'object',
properties: {
entityNames: {
type: 'array',
items: { type: 'string' },
description: 'An array of entity names to delete'
}
},
required: ['entityNames']
}
},
{
name: 'delete_observations',
description: 'Delete specific observations from entities in the knowledge graph',
inputSchema: {
type: 'object',
properties: {
deletions: {
type: 'array',
items: {
type: 'object',
properties: {
entityName: { type: 'string', description: 'The name of the entity containing the observations' },
observations: {
type: 'array',
items: { type: 'string' },
description: 'An array of observations to delete'
}
},
required: ['entityName', 'observations']
}
}
},
required: ['deletions']
}
},
{
name: 'delete_relations',
description: 'Delete multiple relations from the knowledge graph',
inputSchema: {
type: 'object',
properties: {
relations: {
type: 'array',
items: {
type: 'object',
properties: {
from: { type: 'string', description: 'The name of the entity where the relation starts' },
to: { type: 'string', description: 'The name of the entity where the relation ends' },
relationType: { type: 'string', description: 'The type of the relation' }
},
required: ['from', 'to', 'relationType']
},
description: 'An array of relations to delete'
}
},
required: ['relations']
}
},
{
name: 'read_graph',
description: 'Read the entire knowledge graph',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'search_nodes',
description: 'Search for nodes in the knowledge graph based on a query',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'The search query to match against entity names, types, and observation content'
}
},
required: ['query']
}
},
{
name: 'open_nodes',
description: 'Open specific nodes in the knowledge graph by their names',
inputSchema: {
type: 'object',
properties: {
names: {
type: 'array',
items: { type: 'string' },
description: 'An array of entity names to retrieve'
}
},
required: ['names']
}
}
]
}
})
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params
if (!args) {
throw new Error(`No arguments provided for tool: ${name}`)
}
switch (name) {
case 'create_entities':
return {
content: [
{
type: 'text',
text: JSON.stringify(
await this.knowledgeGraphManager.createEntities(args.entities as Entity[]),
null,
2
)
}
]
}
case 'create_relations':
return {
content: [
{
type: 'text',
text: JSON.stringify(
await this.knowledgeGraphManager.createRelations(args.relations as Relation[]),
null,
2
)
}
]
}
case 'add_observations':
return {
content: [
{
type: 'text',
text: JSON.stringify(
await this.knowledgeGraphManager.addObservations(
args.observations as { entityName: string; contents: string[] }[]
),
null,
2
)
}
]
}
case 'delete_entities':
await this.knowledgeGraphManager.deleteEntities(args.entityNames as string[])
return { content: [{ type: 'text', text: 'Entities deleted successfully' }] }
case 'delete_observations':
await this.knowledgeGraphManager.deleteObservations(
args.deletions as { entityName: string; observations: string[] }[]
)
return { content: [{ type: 'text', text: 'Observations deleted successfully' }] }
case 'delete_relations':
await this.knowledgeGraphManager.deleteRelations(args.relations as Relation[])
return { content: [{ type: 'text', text: 'Relations deleted successfully' }] }
case 'read_graph':
return {
content: [{ type: 'text', text: JSON.stringify(await this.knowledgeGraphManager.readGraph(), null, 2) }]
}
case 'search_nodes':
return {
content: [
{
type: 'text',
text: JSON.stringify(await this.knowledgeGraphManager.searchNodes(args.query as string), null, 2)
}
]
}
case 'open_nodes':
return {
content: [
{
type: 'text',
text: JSON.stringify(await this.knowledgeGraphManager.openNodes(args.names as string[]), null, 2)
}
]
}
default:
throw new Error(`Unknown tool: ${name}`)
}
})
}
}
export default MemoryServer

View File

@ -0,0 +1,289 @@
// Sequential Thinking MCP Server
// port https://github.com/modelcontextprotocol/servers/blob/main/src/sequentialthinking/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from '@modelcontextprotocol/sdk/types.js'
// Fixed chalk import for ESM
import chalk from 'chalk'
interface ThoughtData {
thought: string
thoughtNumber: number
totalThoughts: number
isRevision?: boolean
revisesThought?: number
branchFromThought?: number
branchId?: string
needsMoreThoughts?: boolean
nextThoughtNeeded: boolean
}
class SequentialThinkingServer {
private thoughtHistory: ThoughtData[] = []
private branches: Record<string, ThoughtData[]> = {}
private validateThoughtData(input: unknown): ThoughtData {
const data = input as Record<string, unknown>
if (!data.thought || typeof data.thought !== 'string') {
throw new Error('Invalid thought: must be a string')
}
if (!data.thoughtNumber || typeof data.thoughtNumber !== 'number') {
throw new Error('Invalid thoughtNumber: must be a number')
}
if (!data.totalThoughts || typeof data.totalThoughts !== 'number') {
throw new Error('Invalid totalThoughts: must be a number')
}
if (typeof data.nextThoughtNeeded !== 'boolean') {
throw new Error('Invalid nextThoughtNeeded: must be a boolean')
}
return {
thought: data.thought,
thoughtNumber: data.thoughtNumber,
totalThoughts: data.totalThoughts,
nextThoughtNeeded: data.nextThoughtNeeded,
isRevision: data.isRevision as boolean | undefined,
revisesThought: data.revisesThought as number | undefined,
branchFromThought: data.branchFromThought as number | undefined,
branchId: data.branchId as string | undefined,
needsMoreThoughts: data.needsMoreThoughts as boolean | undefined
}
}
private formatThought(thoughtData: ThoughtData): string {
const { thoughtNumber, totalThoughts, thought, isRevision, revisesThought, branchFromThought, branchId } =
thoughtData
let prefix = ''
let context = ''
if (isRevision) {
prefix = chalk.yellow('🔄 Revision')
context = ` (revising thought ${revisesThought})`
} else if (branchFromThought) {
prefix = chalk.green('🌿 Branch')
context = ` (from thought ${branchFromThought}, ID: ${branchId})`
} else {
prefix = chalk.blue('💭 Thought')
context = ''
}
const header = `${prefix} ${thoughtNumber}/${totalThoughts}${context}`
const border = '─'.repeat(Math.max(header.length, thought.length) + 4)
return `
${border}
${header}
${border}
${thought.padEnd(border.length - 2)}
${border}`
}
public processThought(input: unknown): { content: Array<{ type: string; text: string }>; isError?: boolean } {
try {
const validatedInput = this.validateThoughtData(input)
if (validatedInput.thoughtNumber > validatedInput.totalThoughts) {
validatedInput.totalThoughts = validatedInput.thoughtNumber
}
this.thoughtHistory.push(validatedInput)
if (validatedInput.branchFromThought && validatedInput.branchId) {
if (!this.branches[validatedInput.branchId]) {
this.branches[validatedInput.branchId] = []
}
this.branches[validatedInput.branchId].push(validatedInput)
}
const formattedThought = this.formatThought(validatedInput)
console.error(formattedThought)
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
thoughtNumber: validatedInput.thoughtNumber,
totalThoughts: validatedInput.totalThoughts,
nextThoughtNeeded: validatedInput.nextThoughtNeeded,
branches: Object.keys(this.branches),
thoughtHistoryLength: this.thoughtHistory.length
},
null,
2
)
}
]
}
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: error instanceof Error ? error.message : String(error),
status: 'failed'
},
null,
2
)
}
],
isError: true
}
}
}
}
const SEQUENTIAL_THINKING_TOOL: Tool = {
name: 'sequentialthinking',
description: `A detailed tool for dynamic and reflective problem-solving through thoughts.
This tool helps analyze problems through a flexible thinking process that can adapt and evolve.
Each thought can build on, question, or revise previous insights as understanding deepens.
When to use this tool:
- Breaking down complex problems into steps
- Planning and design with room for revision
- Analysis that might need course correction
- Problems where the full scope might not be clear initially
- Problems that require a multi-step solution
- Tasks that need to maintain context over multiple steps
- Situations where irrelevant information needs to be filtered out
Key features:
- You can adjust total_thoughts up or down as you progress
- You can question or revise previous thoughts
- You can add more thoughts even after reaching what seemed like the end
- You can express uncertainty and explore alternative approaches
- Not every thought needs to build linearly - you can branch or backtrack
- Generates a solution hypothesis
- Verifies the hypothesis based on the Chain of Thought steps
- Repeats the process until satisfied
- Provides a correct answer
Parameters explained:
- thought: Your current thinking step, which can include:
* Regular analytical steps
* Revisions of previous thoughts
* Questions about previous decisions
* Realizations about needing more analysis
* Changes in approach
* Hypothesis generation
* Hypothesis verification
- next_thought_needed: True if you need more thinking, even if at what seemed like the end
- thought_number: Current number in sequence (can go beyond initial total if needed)
- total_thoughts: Current estimate of thoughts needed (can be adjusted up/down)
- is_revision: A boolean indicating if this thought revises previous thinking
- revises_thought: If is_revision is true, which thought number is being reconsidered
- branch_from_thought: If branching, which thought number is the branching point
- branch_id: Identifier for the current branch (if any)
- needs_more_thoughts: If reaching end but realizing more thoughts needed
You should:
1. Start with an initial estimate of needed thoughts, but be ready to adjust
2. Feel free to question or revise previous thoughts
3. Don't hesitate to add more thoughts if needed, even at the "end"
4. Express uncertainty when present
5. Mark thoughts that revise previous thinking or branch into new paths
6. Ignore information that is irrelevant to the current step
7. Generate a solution hypothesis when appropriate
8. Verify the hypothesis based on the Chain of Thought steps
9. Repeat the process until satisfied with the solution
10. Provide a single, ideally correct answer as the final output
11. Only set next_thought_needed to false when truly done and a satisfactory answer is reached`,
inputSchema: {
type: 'object',
properties: {
thought: {
type: 'string',
description: 'Your current thinking step'
},
nextThoughtNeeded: {
type: 'boolean',
description: 'Whether another thought step is needed'
},
thoughtNumber: {
type: 'integer',
description: 'Current thought number',
minimum: 1
},
totalThoughts: {
type: 'integer',
description: 'Estimated total thoughts needed',
minimum: 1
},
isRevision: {
type: 'boolean',
description: 'Whether this revises previous thinking'
},
revisesThought: {
type: 'integer',
description: 'Which thought is being reconsidered',
minimum: 1
},
branchFromThought: {
type: 'integer',
description: 'Branching point thought number',
minimum: 1
},
branchId: {
type: 'string',
description: 'Branch identifier'
},
needsMoreThoughts: {
type: 'boolean',
description: 'If more thoughts are needed'
}
},
required: ['thought', 'nextThoughtNeeded', 'thoughtNumber', 'totalThoughts']
}
}
class ThinkingServer {
public server: Server
private thinkingServer: SequentialThinkingServer
constructor() {
this.thinkingServer = new SequentialThinkingServer()
this.server = new Server(
{
name: 'sequential-thinking-server',
version: '0.2.0'
},
{
capabilities: {
tools: {}
}
}
)
this.initialize()
}
initialize() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [SEQUENTIAL_THINKING_TOOL]
}))
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'sequentialthinking') {
return this.thinkingServer.processThought(request.params.arguments)
}
return {
content: [
{
type: 'text',
text: `Unknown tool: ${request.params.name}`
}
],
isError: true
}
})
}
}
export default ThinkingServer

View File

@ -2,11 +2,13 @@ import os from 'node:os'
import path from 'node:path' import path from 'node:path'
import { isLinux, isMac, isWin } from '@main/constant' import { isLinux, isMac, isWin } from '@main/constant'
import { createInMemoryMCPServer } from '@main/mcpServers/factory'
import { makeSureDirExists } from '@main/utils' import { makeSureDirExists } from '@main/utils'
import { getBinaryName, getBinaryPath } from '@main/utils/process' import { getBinaryName, getBinaryPath } from '@main/utils/process'
import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory'
import { nanoid } from '@reduxjs/toolkit' import { nanoid } from '@reduxjs/toolkit'
import { MCPServer, MCPTool } from '@types' import { MCPServer, MCPTool } from '@types'
import { app } from 'electron' import { app } from 'electron'
@ -61,11 +63,25 @@ class McpService {
const args = [...(server.args || [])] const args = [...(server.args || [])]
let transport: StdioClientTransport | SSEClientTransport let transport: StdioClientTransport | SSEClientTransport | InMemoryTransport
try { try {
// Create appropriate transport based on configuration // Create appropriate transport based on configuration
if (server.baseUrl) { if (server.type === 'inMemory') {
Logger.info(`[MCP] Using in-memory transport for server: ${server.name}`)
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
// start the in-memory server with the given name and environment variables
const inMemoryServer = createInMemoryMCPServer(server.name, args, server.env || {})
try {
await inMemoryServer.connect(serverTransport)
Logger.info(`[MCP] In-memory server started: ${server.name}`)
} catch (error) {
Logger.error(`[MCP] Error starting in-memory server: ${error}`)
throw new Error(`Failed to start in-memory server: ${error}`)
}
// set the client transport to the client
transport = clientTransport
} else if (server.baseUrl) {
transport = new SSEClientTransport(new URL(server.baseUrl)) transport = new SSEClientTransport(new URL(server.baseUrl))
} else if (server.command) { } else if (server.command) {
let cmd = server.command let cmd = server.command

View File

@ -1,4 +1,5 @@
import * as fs from 'node:fs' import * as fs from 'node:fs'
import os from 'node:os'
import path from 'node:path' import path from 'node:path'
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@shared/config/constant' import { audioExts, documentExts, imageExts, textExts, videoExts } from '@shared/config/constant'
@ -74,3 +75,7 @@ export function getTempDir() {
export function getFilesDir() { export function getFilesDir() {
return path.join(app.getPath('userData'), 'Data', 'Files') return path.join(app.getPath('userData'), 'Data', 'Files')
} }
export function getConfigDir() {
return path.join(os.homedir(), '.cherrystudio', 'config')
}

View File

@ -1,10 +1,10 @@
<!doctype html> <!doctype html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1, width=device-width" /> <meta name="viewport" content="initial-scale=1, width=device-width" />
<meta http-equiv="Content-Security-Policy" <meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" /> content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
<title>Cherry Studio</title> <title>Cherry Studio</title>

View File

@ -21,8 +21,9 @@
h6 { h6 {
margin: 1em 0 1em 0; margin: 1em 0 1em 0;
font-weight: 800; font-weight: 800;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', font-family:
'Helvetica Neue', sans-serif; -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue',
sans-serif;
} }
h1 { h1 {
@ -170,8 +171,9 @@
th { th {
background-color: var(--color-background-mute); background-color: var(--color-background-mute);
font-weight: bold; font-weight: bold;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', font-family:
'Helvetica Neue', sans-serif; -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue',
sans-serif;
} }
img { img {

View File

@ -999,6 +999,9 @@
"argsTooltip": "Each argument on a new line", "argsTooltip": "Each argument on a new line",
"baseUrlTooltip": "Remote server base URL", "baseUrlTooltip": "Remote server base URL",
"command": "Command", "command": "Command",
"sse": "Server-Sent Events(sse)",
"stdio": "Standard Input/Output(stdio)",
"inMemory": "Memory",
"config_description": "Configure Model Context Protocol servers", "config_description": "Configure Model Context Protocol servers",
"deleteError": "Failed to delete server", "deleteError": "Failed to delete server",
"deleteSuccess": "Server deleted successfully", "deleteSuccess": "Server deleted successfully",

View File

@ -998,6 +998,9 @@
"argsTooltip": "1行に1つの引数を入力してください", "argsTooltip": "1行に1つの引数を入力してください",
"baseUrlTooltip": "リモートURLアドレス", "baseUrlTooltip": "リモートURLアドレス",
"command": "コマンド", "command": "コマンド",
"sse": "サーバー送信イベント(sse)",
"stdio": "標準入力/出力(stdio)",
"inMemory": "メモリ",
"config_description": "モデルコンテキストプロトコルサーバーの設定", "config_description": "モデルコンテキストプロトコルサーバーの設定",
"deleteError": "サーバーの削除に失敗しました", "deleteError": "サーバーの削除に失敗しました",
"deleteSuccess": "サーバーが正常に削除されました", "deleteSuccess": "サーバーが正常に削除されました",

View File

@ -998,6 +998,9 @@
"argsTooltip": "Каждый аргумент с новой строки", "argsTooltip": "Каждый аргумент с новой строки",
"baseUrlTooltip": "Адрес удаленного URL", "baseUrlTooltip": "Адрес удаленного URL",
"command": "Команда", "command": "Команда",
"sse": "События, отправляемые сервером(sse)",
"stdio": "Стандартный ввод/вывод(stdio)",
"inMemory": "Память",
"config_description": "Настройка серверов протокола контекста модели", "config_description": "Настройка серверов протокола контекста модели",
"deleteError": "Не удалось удалить сервер", "deleteError": "Не удалось удалить сервер",
"deleteSuccess": "Сервер успешно удален", "deleteSuccess": "Сервер успешно удален",

View File

@ -999,6 +999,9 @@
"argsTooltip": "每个参数占一行", "argsTooltip": "每个参数占一行",
"baseUrlTooltip": "远程 URL 地址", "baseUrlTooltip": "远程 URL 地址",
"command": "命令", "command": "命令",
"sse": "服务器发送事件(sse)",
"stdio": "标准输入/输出(stdio)",
"inMemory": "内存",
"config_description": "配置模型上下文协议服务器", "config_description": "配置模型上下文协议服务器",
"deleteError": "删除服务器失败", "deleteError": "删除服务器失败",
"deleteSuccess": "服务器删除成功", "deleteSuccess": "服务器删除成功",

View File

@ -998,6 +998,9 @@
"argsTooltip": "每個參數佔一行", "argsTooltip": "每個參數佔一行",
"baseUrlTooltip": "遠端 URL 地址", "baseUrlTooltip": "遠端 URL 地址",
"command": "指令", "command": "指令",
"sse": "伺服器傳送事件(sse)",
"stdio": "標準輸入/輸出(stdio)",
"inMemory": "記憶體",
"config_description": "設定模型上下文協議伺服器", "config_description": "設定模型上下文協議伺服器",
"deleteError": "刪除伺服器失敗", "deleteError": "刪除伺服器失敗",
"deleteSuccess": "伺服器刪除成功", "deleteSuccess": "伺服器刪除成功",

View File

@ -1,9 +1,11 @@
import { CodeOutlined } from '@ant-design/icons' import { CodeOutlined } from '@ant-design/icons'
import { useMCPServers } from '@renderer/hooks/useMCPServers' import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { initializeMCPServers } from '@renderer/store/mcp'
import { MCPServer } from '@renderer/types' import { MCPServer } from '@renderer/types'
import { Dropdown, Switch, Tooltip } from 'antd' import { Dropdown, Switch, Tooltip } from 'antd'
import { FC, useRef, useState } from 'react' import { FC, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import styled from 'styled-components' import styled from 'styled-components'
interface Props { interface Props {
@ -13,16 +15,21 @@ interface Props {
} }
const MCPToolsButton: FC<Props> = ({ enabledMCPs, toggelEnableMCP, ToolbarButton }) => { const MCPToolsButton: FC<Props> = ({ enabledMCPs, toggelEnableMCP, ToolbarButton }) => {
const { activedMcpServers } = useMCPServers() const { activedMcpServers, mcpServers } = useMCPServers()
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef<any>(null) const dropdownRef = useRef<any>(null)
const menuRef = useRef<HTMLDivElement>(null) const menuRef = useRef<HTMLDivElement>(null)
const { t } = useTranslation() const { t } = useTranslation()
const dispatch = useDispatch()
const availableMCPs = activedMcpServers.filter((server) => enabledMCPs.some((s) => s.id === server.id)) const availableMCPs = activedMcpServers.filter((server) => enabledMCPs.some((s) => s.id === server.id))
const buttonEnabled = availableMCPs.length > 0 const buttonEnabled = availableMCPs.length > 0
useEffect(() => {
initializeMCPServers(mcpServers, dispatch)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const truncateText = (text: string, maxLength: number = 50) => { const truncateText = (text: string, maxLength: number = 50) => {
if (!text || text.length <= maxLength) return text if (!text || text.length <= maxLength) return text
return text.substring(0, maxLength) + '...' return text.substring(0, maxLength) + '...'

View File

@ -17,7 +17,7 @@ interface Props {
interface MCPFormValues { interface MCPFormValues {
name: string name: string
description?: string description?: string
serverType: 'sse' | 'stdio' serverType: MCPServer['type']
baseUrl?: string baseUrl?: string
command?: string command?: string
registryUrl?: string registryUrl?: string
@ -42,19 +42,19 @@ const PipRegistry: Registry[] = [
const McpSettings: React.FC<Props> = ({ server }) => { const McpSettings: React.FC<Props> = ({ server }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { deleteMCPServer } = useMCPServers() const { deleteMCPServer, updateMCPServer } = useMCPServers()
const [serverType, setServerType] = useState<'sse' | 'stdio'>('stdio') const [serverType, setServerType] = useState<MCPServer['type']>('stdio')
const [form] = Form.useForm<MCPFormValues>() const [form] = Form.useForm<MCPFormValues>()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [isFormChanged, setIsFormChanged] = useState(false) const [isFormChanged, setIsFormChanged] = useState(false)
const [loadingServer, setLoadingServer] = useState<string | null>(null) const [loadingServer, setLoadingServer] = useState<string | null>(null)
const { updateMCPServer } = useMCPServers()
const [tools, setTools] = useState<MCPTool[]>([]) const [tools, setTools] = useState<MCPTool[]>([])
const [isShowRegistry, setIsShowRegistry] = useState(false) const [isShowRegistry, setIsShowRegistry] = useState(false)
const [registry, setRegistry] = useState<Registry[]>() const [registry, setRegistry] = useState<Registry[]>()
useEffect(() => { useEffect(() => {
const serverType = server.baseUrl ? 'sse' : 'stdio' const serverType: MCPServer['type'] = server.type || (server.baseUrl ? 'sse' : 'stdio')
setServerType(serverType) setServerType(serverType)
// Set registry UI state based on command and registryUrl // Set registry UI state based on command and registryUrl
@ -103,7 +103,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [form.getFieldValue('serverType')]) }, [form.getFieldValue('serverType')])
const fetchTools = useCallback(async () => { const fetchTools = async () => {
if (server.isActive) { if (server.isActive) {
try { try {
setLoadingServer(server.id) setLoadingServer(server.id)
@ -119,15 +119,14 @@ const McpSettings: React.FC<Props> = ({ server }) => {
setLoadingServer(null) setLoadingServer(null)
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }
}, [server.id])
useEffect(() => { useEffect(() => {
console.log('Loading tools for server:', server.id, 'Active:', server.isActive)
if (server.isActive) { if (server.isActive) {
fetchTools() fetchTools()
} }
}, [server.id, server.isActive, fetchTools]) // eslint-disable-next-line react-hooks/exhaustive-deps
}, [server.id, server.isActive])
// Save the form data // Save the form data
const onSave = async () => { const onSave = async () => {
@ -139,6 +138,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
const mcpServer: MCPServer = { const mcpServer: MCPServer = {
id: server.id, id: server.id,
name: values.name, name: values.name,
type: values.serverType,
description: values.description, description: values.description,
isActive: values.isActive, isActive: values.isActive,
registryUrl: values.registryUrl registryUrl: values.registryUrl
@ -171,7 +171,6 @@ const McpSettings: React.FC<Props> = ({ server }) => {
await window.api.mcp.restartServer(mcpServer) await window.api.mcp.restartServer(mcpServer)
updateMCPServer({ ...mcpServer, isActive: true }) updateMCPServer({ ...mcpServer, isActive: true })
window.message.success({ content: t('settings.mcp.updateSuccess'), key: 'mcp-update-success' }) window.message.success({ content: t('settings.mcp.updateSuccess'), key: 'mcp-update-success' })
await fetchTools()
setLoading(false) setLoading(false)
setIsFormChanged(false) setIsFormChanged(false)
} catch (error: any) { } catch (error: any) {
@ -312,7 +311,9 @@ const McpSettings: React.FC<Props> = ({ server }) => {
<SettingTitle> <SettingTitle>
<Flex justify="space-between" align="center" gap={5} style={{ marginRight: 10 }}> <Flex justify="space-between" align="center" gap={5} style={{ marginRight: 10 }}>
<ServerName className="text-nowrap">{server?.name}</ServerName> <ServerName className="text-nowrap">{server?.name}</ServerName>
{!(server.type === 'inMemory') && (
<Button danger icon={<DeleteOutlined />} type="text" onClick={() => onDeleteMcpServer(server)} /> <Button danger icon={<DeleteOutlined />} type="text" onClick={() => onDeleteMcpServer(server)} />
)}
</Flex> </Flex>
<Flex align="center" gap={16}> <Flex align="center" gap={16}>
<Switch <Switch
@ -347,8 +348,9 @@ const McpSettings: React.FC<Props> = ({ server }) => {
<Radio.Group <Radio.Group
onChange={(e) => setServerType(e.target.value)} onChange={(e) => setServerType(e.target.value)}
options={[ options={[
{ label: 'STDIO', value: 'stdio' }, { label: t('settings.mcp.stdio'), value: 'stdio' },
{ label: 'SSE', value: 'sse' } { label: t('settings.mcp.sse'), value: 'sse' },
{ label: t('settings.mcp.inMemory'), value: 'inMemory' }
]} ]}
/> />
</Form.Item> </Form.Item>
@ -402,6 +404,17 @@ const McpSettings: React.FC<Props> = ({ server }) => {
<TextArea rows={3} placeholder={`arg1\narg2`} style={{ fontFamily: 'monospace' }} /> <TextArea rows={3} placeholder={`arg1\narg2`} style={{ fontFamily: 'monospace' }} />
</Form.Item> </Form.Item>
<Form.Item name="env" label={t('settings.mcp.env')} tooltip={t('settings.mcp.envTooltip')}>
<TextArea rows={3} placeholder={`KEY1=value1\nKEY2=value2`} style={{ fontFamily: 'monospace' }} />
</Form.Item>
</>
)}
{serverType === 'inMemory' && (
<>
<Form.Item name="args" label={t('settings.mcp.args')} tooltip={t('settings.mcp.argsTooltip')}>
<TextArea rows={3} placeholder={`arg1\narg2`} style={{ fontFamily: 'monospace' }} />
</Form.Item>
<Form.Item name="env" label={t('settings.mcp.env')} tooltip={t('settings.mcp.envTooltip')}> <Form.Item name="env" label={t('settings.mcp.env')} tooltip={t('settings.mcp.envTooltip')}>
<TextArea rows={3} placeholder={`KEY1=value1\nKEY2=value2`} style={{ fontFamily: 'monospace' }} /> <TextArea rows={3} placeholder={`KEY1=value1\nKEY2=value2`} style={{ fontFamily: 'monospace' }} />
</Form.Item> </Form.Item>

View File

@ -8,11 +8,13 @@ import Scrollbar from '@renderer/components/Scrollbar'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useMCPServers } from '@renderer/hooks/useMCPServers' import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { EventEmitter } from '@renderer/services/EventService' import { EventEmitter } from '@renderer/services/EventService'
import { initializeMCPServers } from '@renderer/store/mcp'
import { MCPServer } from '@renderer/types' import { MCPServer } from '@renderer/types'
import { Dropdown, MenuProps } from 'antd' import { Dropdown, MenuProps } from 'antd'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import { FC, useCallback, useEffect, useMemo, useState } from 'react' import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingContainer } from '..' import { SettingContainer } from '..'
@ -26,6 +28,7 @@ const MCPSettings: FC = () => {
const [selectedMcpServer, setSelectedMcpServer] = useState<MCPServer | null>(mcpServers[0]) const [selectedMcpServer, setSelectedMcpServer] = useState<MCPServer | null>(mcpServers[0])
const [route, setRoute] = useState<'npx-search' | 'mcp-install' | null>(null) const [route, setRoute] = useState<'npx-search' | 'mcp-install' | null>(null)
const { theme } = useTheme() const { theme } = useTheme()
const dispatch = useDispatch()
useEffect(() => { useEffect(() => {
const unsubs = [ const unsubs = [
@ -35,6 +38,11 @@ const MCPSettings: FC = () => {
return () => unsubs.forEach((unsub) => unsub()) return () => unsubs.forEach((unsub) => unsub())
}, []) }, [])
useEffect(() => {
initializeMCPServers(mcpServers, dispatch)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) // Empty dependency array to run only once
const onAddMcpServer = async () => { const onAddMcpServer = async () => {
const newServer = { const newServer = {
id: nanoid(), id: nanoid(),

View File

@ -1,21 +1,8 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit' import { createSlice, nanoid, type PayloadAction } from '@reduxjs/toolkit'
import { nanoid } from '@reduxjs/toolkit'
import type { MCPConfig, MCPServer } from '@renderer/types' import type { MCPConfig, MCPServer } from '@renderer/types'
const initialState: MCPConfig = { export const initialState: MCPConfig = {
servers: [ servers: []
{
id: nanoid(),
name: 'mcp-auto-install',
description: 'Automatically install MCP services (Beta version)',
baseUrl: '',
command: 'npx',
args: ['-y', '@mcpmarket/mcp-auto-install', 'connect', '--json'],
registryUrl: 'https://registry.npmmirror.com',
env: {},
isActive: false
}
]
} }
const mcpSlice = createSlice({ const mcpSlice = createSlice({
@ -63,3 +50,79 @@ export const selectMCP = (state: { mcp: MCPConfig }) => state.mcp
export { mcpSlice } export { mcpSlice }
// Export the reducer as default export // Export the reducer as default export
export default mcpSlice.reducer export default mcpSlice.reducer
export const builtinMCPServers: MCPServer[] = [
{
id: nanoid(),
name: '@cherry/mcp-auto-install',
description: 'Automatically install MCP services (Beta version)',
command: 'npx',
args: ['-y', '@mcpmarket/mcp-auto-install', 'connect', '--json'],
isActive: false
},
{
id: nanoid(),
name: '@cherry/memory',
type: 'inMemory',
description:
'A basic implementation of persistent memory using a local knowledge graph. This lets Claude remember information about the user across chats. https://github.com/modelcontextprotocol/servers/tree/main/src/memory',
isActive: true
},
{
id: nanoid(),
name: '@cherry/sequentialthinking',
type: 'inMemory',
description:
'An MCP server implementation that provides a tool for dynamic and reflective problem-solving through a structured thinking process.',
isActive: true
},
{
id: nanoid(),
name: '@cherry/brave-search',
type: 'inMemory',
description:
'An MCP server implementation that integrates the Brave Search API, providing both web and local search capabilities.',
isActive: false
},
{
id: nanoid(),
name: '@cherry/everything',
type: 'inMemory',
description:
'This MCP server attempts to exercise all the features of the MCP protocol. It is not intended to be a useful server, but rather a test server for builders of MCP clients. It implements prompts, tools, resources, sampling, and more to showcase MCP capabilities.',
isActive: true
},
{
id: nanoid(),
name: '@cherry/fetch',
type: 'inMemory',
description: 'An MCP server for fetching URLs / Youtube video transcript.',
isActive: true
},
{
id: nanoid(),
name: '@cherry/filesystem',
type: 'inMemory',
description: 'Node.js server implementing Model Context Protocol (MCP) for filesystem operations.',
isActive: false
}
]
/**
* Utility function to add servers to the MCP store during app initialization
* @param servers Array of MCP servers to add
* @param dispatch Redux dispatch function
*/
export const initializeMCPServers = (existingServers: MCPServer[], dispatch: (action: any) => void): void => {
// Check if the existing servers already contain the built-in servers
const serverIds = new Set(existingServers.map((server) => server.name))
// Filter out any built-in servers that are already present
const newServers = builtinMCPServers.filter((server) => !serverIds.has(server.name))
console.log('Adding new servers:', newServers)
// Add the new built-in servers to the existing servers
newServers.forEach((server) => {
dispatch(addMCPServer(server))
})
}

View File

@ -227,6 +227,7 @@ export type AppInfo = {
version: string version: string
isPackaged: boolean isPackaged: boolean
appPath: string appPath: string
configPath: string
appDataPath: string appDataPath: string
resourcesPath: string resourcesPath: string
filesPath: string filesPath: string
@ -365,6 +366,7 @@ export interface MCPServerParameter {
export interface MCPServer { export interface MCPServer {
id: string id: string
name: string name: string
type?: 'stdio' | 'sse' | 'inMemory'
description?: string description?: string
baseUrl?: string baseUrl?: string
command?: string command?: string

View File

@ -15,9 +15,7 @@ import {
SimpleStringSchema, SimpleStringSchema,
Tool as geminiTool Tool as geminiTool
} from '@google/generative-ai' } from '@google/generative-ai'
import { nanoid } from '@reduxjs/toolkit'
import store from '@renderer/store' import store from '@renderer/store'
import { addMCPServer } from '@renderer/store/mcp'
import { MCPServer, MCPTool, MCPToolResponse } from '@renderer/types' import { MCPServer, MCPTool, MCPToolResponse } from '@renderer/types'
import { ChatCompletionMessageToolCall, ChatCompletionTool } from 'openai/resources' import { ChatCompletionMessageToolCall, ChatCompletionTool } from 'openai/resources'
@ -234,24 +232,6 @@ export async function callMCPTool(tool: MCPTool): Promise<any> {
}) })
console.log(`[MCP] Tool called: ${tool.serverName} ${tool.name}`, resp) console.log(`[MCP] Tool called: ${tool.serverName} ${tool.name}`, resp)
if (tool.serverName === 'mcp-auto-install') {
if (resp.data) {
const mcpServer: MCPServer = {
id: `f${nanoid()}`,
name: resp.data.name,
description: resp.data.description,
baseUrl: resp.data.baseUrl,
command: resp.data.command,
args: resp.data.args,
env: resp.data.env,
registryUrl: '',
isActive: false
}
store.dispatch(addMCPServer(mcpServer))
}
}
return resp return resp
} catch (e) { } catch (e) {
console.error(`[MCP] Error calling Tool: ${tool.serverName} ${tool.name}`, e) console.error(`[MCP] Error calling Tool: ${tool.serverName} ${tool.name}`, e)

View File

@ -1,9 +1,8 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Selection Menu</title> <title>Selection Menu</title>
<style> <style>
:root { :root {
@ -134,7 +133,7 @@
</menu> </menu>
<script> <script>
document.querySelectorAll('button').forEach(button => { document.querySelectorAll('button').forEach((button) => {
button.addEventListener('click', () => { button.addEventListener('click', () => {
const action = button.getAttribute('data-action') const action = button.getAttribute('data-action')
window.api.selectionMenu.action(action) window.api.selectionMenu.action(action)
@ -142,5 +141,4 @@
}) })
</script> </script>
</body> </body>
</html> </html>

508
yarn.lock
View File

@ -191,6 +191,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@asamuzakjp/css-color@npm:^3.1.1":
version: 3.1.1
resolution: "@asamuzakjp/css-color@npm:3.1.1"
dependencies:
"@csstools/css-calc": "npm:^2.1.2"
"@csstools/css-color-parser": "npm:^3.0.8"
"@csstools/css-parser-algorithms": "npm:^3.0.4"
"@csstools/css-tokenizer": "npm:^3.0.3"
lru-cache: "npm:^10.4.3"
checksum: 10c0/4abb010fd29de8acae8571eba738468c22cb45a1f77647df3c59a80f1c83d83d728cae3ebbf99e5c73f2517761abaaffbe5e4176fc46b5f9bf60f1478463b51e
languageName: node
linkType: hard
"@babel/code-frame@npm:^7.26.2": "@babel/code-frame@npm:^7.26.2":
version: 7.26.2 version: 7.26.2
resolution: "@babel/code-frame@npm:7.26.2" resolution: "@babel/code-frame@npm:7.26.2"
@ -632,6 +645,52 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@csstools/color-helpers@npm:^5.0.2":
version: 5.0.2
resolution: "@csstools/color-helpers@npm:5.0.2"
checksum: 10c0/bebaddb28b9eb58b0449edd5d0c0318fa88f3cb079602ee27e88c9118070d666dcc4e09a5aa936aba2fde6ba419922ade07b7b506af97dd7051abd08dfb2959b
languageName: node
linkType: hard
"@csstools/css-calc@npm:^2.1.2":
version: 2.1.2
resolution: "@csstools/css-calc@npm:2.1.2"
peerDependencies:
"@csstools/css-parser-algorithms": ^3.0.4
"@csstools/css-tokenizer": ^3.0.3
checksum: 10c0/34ced30553968ef5d5f9e00e3b90b48c47480cf130e282e99d57ec9b09f803aab8bc06325683e72a1518b5e7180a3da8b533f1b462062757c21989a53b482e1a
languageName: node
linkType: hard
"@csstools/css-color-parser@npm:^3.0.8":
version: 3.0.8
resolution: "@csstools/css-color-parser@npm:3.0.8"
dependencies:
"@csstools/color-helpers": "npm:^5.0.2"
"@csstools/css-calc": "npm:^2.1.2"
peerDependencies:
"@csstools/css-parser-algorithms": ^3.0.4
"@csstools/css-tokenizer": ^3.0.3
checksum: 10c0/90722c5a62ca94e9d578ddf59be604a76400b932bd3d4bd23cb1ae9b7ace8fcf83c06995d2b31f96f4afef24a7cefba79beb11ed7ee4999d7ecfec3869368359
languageName: node
linkType: hard
"@csstools/css-parser-algorithms@npm:^3.0.4":
version: 3.0.4
resolution: "@csstools/css-parser-algorithms@npm:3.0.4"
peerDependencies:
"@csstools/css-tokenizer": ^3.0.3
checksum: 10c0/d411f07765e14eede17bccc6bd4f90ff303694df09aabfede3fd104b2dfacfd4fe3697cd25ddad14684c850328f3f9420ebfa9f78380892492974db24ae47dbd
languageName: node
linkType: hard
"@csstools/css-tokenizer@npm:^3.0.3":
version: 3.0.3
resolution: "@csstools/css-tokenizer@npm:3.0.3"
checksum: 10c0/c31bf410e1244b942e71798e37c54639d040cb59e0121b21712b40015fced2b0fb1ffe588434c5f8923c9cd0017cfc1c1c8f3921abc94c96edf471aac2eba5e5
languageName: node
linkType: hard
"@develar/schema-utils@npm:~2.6.5": "@develar/schema-utils@npm:~2.6.5":
version: 2.6.5 version: 2.6.5
resolution: "@develar/schema-utils@npm:2.6.5" resolution: "@develar/schema-utils@npm:2.6.5"
@ -2369,6 +2428,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mixmark-io/domino@npm:^2.2.0":
version: 2.2.0
resolution: "@mixmark-io/domino@npm:2.2.0"
checksum: 10c0/aa468a15f9217d425220fe6a4b3f9416cbe8e566ee14efc191c6d5cc04fe39338b16a90bbac190f28d44e69465db5f2cf95f479c621ce38060ca6b2a3d346e9d
languageName: node
linkType: hard
"@modelcontextprotocol/sdk@npm:^1.8.0": "@modelcontextprotocol/sdk@npm:^1.8.0":
version: 1.8.0 version: 1.8.0
resolution: "@modelcontextprotocol/sdk@npm:1.8.0" resolution: "@modelcontextprotocol/sdk@npm:1.8.0"
@ -2922,6 +2988,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@sec-ant/readable-stream@npm:^0.4.1":
version: 0.4.1
resolution: "@sec-ant/readable-stream@npm:0.4.1"
checksum: 10c0/64e9e9cf161e848067a5bf60cdc04d18495dc28bb63a8d9f8993e4dd99b91ad34e4b563c85de17d91ffb177ec17a0664991d2e115f6543e73236a906068987af
languageName: node
linkType: hard
"@selderee/plugin-htmlparser2@npm:^0.11.0": "@selderee/plugin-htmlparser2@npm:^0.11.0":
version: 0.11.0 version: 0.11.0
resolution: "@selderee/plugin-htmlparser2@npm:0.11.0" resolution: "@selderee/plugin-htmlparser2@npm:0.11.0"
@ -3002,20 +3075,27 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@sindresorhus/is@npm:^4.0.0": "@sindresorhus/is@npm:^4.0.0, @sindresorhus/is@npm:^4.2.0":
version: 4.6.0 version: 4.6.0
resolution: "@sindresorhus/is@npm:4.6.0" resolution: "@sindresorhus/is@npm:4.6.0"
checksum: 10c0/33b6fb1d0834ec8dd7689ddc0e2781c2bfd8b9c4e4bacbcb14111e0ae00621f2c264b8a7d36541799d74888b5dccdf422a891a5cb5a709ace26325eedc81e22e checksum: 10c0/33b6fb1d0834ec8dd7689ddc0e2781c2bfd8b9c4e4bacbcb14111e0ae00621f2c264b8a7d36541799d74888b5dccdf422a891a5cb5a709ace26325eedc81e22e
languageName: node languageName: node
linkType: hard linkType: hard
"@sindresorhus/is@npm:^5.2.0": "@sindresorhus/is@npm:^5.2.0, @sindresorhus/is@npm:^5.3.0":
version: 5.6.0 version: 5.6.0
resolution: "@sindresorhus/is@npm:5.6.0" resolution: "@sindresorhus/is@npm:5.6.0"
checksum: 10c0/66727344d0c92edde5760b5fd1f8092b717f2298a162a5f7f29e4953e001479927402d9d387e245fb9dc7d3b37c72e335e93ed5875edfc5203c53be8ecba1b52 checksum: 10c0/66727344d0c92edde5760b5fd1f8092b717f2298a162a5f7f29e4953e001479927402d9d387e245fb9dc7d3b37c72e335e93ed5875edfc5203c53be8ecba1b52
languageName: node languageName: node
linkType: hard linkType: hard
"@sindresorhus/is@npm:^7.0.1":
version: 7.0.1
resolution: "@sindresorhus/is@npm:7.0.1"
checksum: 10c0/6d43a916d70d9b64066394c272883869b22faf21f4748aaf399c1b691ea704ea607d1668ff2eb5704e5be8809c4a7faafe16be048ce5e1a2ba6e8928b8e3461c
languageName: node
linkType: hard
"@szmarczak/http-timer@npm:^4.0.5": "@szmarczak/http-timer@npm:^4.0.5":
version: 4.0.6 version: 4.0.6
resolution: "@szmarczak/http-timer@npm:4.0.6" resolution: "@szmarczak/http-timer@npm:4.0.6"
@ -3226,6 +3306,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/diff@npm:^7":
version: 7.0.2
resolution: "@types/diff@npm:7.0.2"
checksum: 10c0/ac4de3f982242292e006ace98a9d41363ebc244145939466139828ffa6c476acc15eea2bad39bd7e0868003c497614f6d7e734d4999c4f09d95dfd173d24d723
languageName: node
linkType: hard
"@types/estree-jsx@npm:^1.0.0": "@types/estree-jsx@npm:^1.0.0":
version: 1.0.5 version: 1.0.5
resolution: "@types/estree-jsx@npm:1.0.5" resolution: "@types/estree-jsx@npm:1.0.5"
@ -3280,7 +3367,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/http-cache-semantics@npm:*, @types/http-cache-semantics@npm:^4.0.2": "@types/http-cache-semantics@npm:*, @types/http-cache-semantics@npm:^4.0.2, @types/http-cache-semantics@npm:^4.0.4":
version: 4.0.4 version: 4.0.4
resolution: "@types/http-cache-semantics@npm:4.0.4" resolution: "@types/http-cache-semantics@npm:4.0.4"
checksum: 10c0/51b72568b4b2863e0fe8d6ce8aad72a784b7510d72dc866215642da51d84945a9459fa89f49ec48f1e9a1752e6a78e85a4cda0ded06b1c73e727610c925f9ce6 checksum: 10c0/51b72568b4b2863e0fe8d6ce8aad72a784b7510d72dc866215642da51d84945a9459fa89f49ec48f1e9a1752e6a78e85a4cda0ded06b1c73e727610c925f9ce6
@ -3777,6 +3864,7 @@ __metadata:
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch" "@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch"
"@tryfabric/martian": "npm:^1.2.4" "@tryfabric/martian": "npm:^1.2.4"
"@types/adm-zip": "npm:^0" "@types/adm-zip": "npm:^0"
"@types/diff": "npm:^7"
"@types/fs-extra": "npm:^11" "@types/fs-extra": "npm:^11"
"@types/lodash": "npm:^4.17.5" "@types/lodash": "npm:^4.17.5"
"@types/markdown-it": "npm:^14" "@types/markdown-it": "npm:^14"
@ -3798,6 +3886,7 @@ __metadata:
dayjs: "npm:^1.11.11" dayjs: "npm:^1.11.11"
dexie: "npm:^4.0.8" dexie: "npm:^4.0.8"
dexie-react-hooks: "npm:^1.1.7" dexie-react-hooks: "npm:^1.1.7"
diff: "npm:^7.0.0"
docx: "npm:^9.0.2" docx: "npm:^9.0.2"
dotenv-cli: "npm:^7.4.2" dotenv-cli: "npm:^7.4.2"
electron: "npm:31.7.6" electron: "npm:31.7.6"
@ -3819,9 +3908,11 @@ __metadata:
fast-xml-parser: "npm:^5.0.9" fast-xml-parser: "npm:^5.0.9"
fetch-socks: "npm:^1.3.2" fetch-socks: "npm:^1.3.2"
fs-extra: "npm:^11.2.0" fs-extra: "npm:^11.2.0"
got-scraping: "npm:^4.1.1"
html-to-image: "npm:^1.11.13" html-to-image: "npm:^1.11.13"
husky: "npm:^9.1.7" husky: "npm:^9.1.7"
i18next: "npm:^23.11.5" i18next: "npm:^23.11.5"
jsdom: "npm:^26.0.0"
lint-staged: "npm:^15.5.0" lint-staged: "npm:^15.5.0"
lodash: "npm:^4.17.21" lodash: "npm:^4.17.21"
markdown-it: "npm:^14.1.0" markdown-it: "npm:^14.1.0"
@ -3859,6 +3950,8 @@ __metadata:
tar: "npm:^7.4.3" tar: "npm:^7.4.3"
tinycolor2: "npm:^1.6.0" tinycolor2: "npm:^1.6.0"
tokenx: "npm:^0.4.1" tokenx: "npm:^0.4.1"
turndown: "npm:^7.2.0"
turndown-plugin-gfm: "npm:^1.0.2"
typescript: "npm:^5.6.2" typescript: "npm:^5.6.2"
undici: "npm:^7.4.0" undici: "npm:^7.4.0"
uuid: "npm:^10.0.0" uuid: "npm:^10.0.0"
@ -3926,7 +4019,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"adm-zip@npm:^0.5.16": "adm-zip@npm:^0.5.16, adm-zip@npm:^0.5.9":
version: 0.5.16 version: 0.5.16
resolution: "adm-zip@npm:0.5.16" resolution: "adm-zip@npm:0.5.16"
checksum: 10c0/6f10119d4570c7ba76dcf428abb8d3f69e63f92e51f700a542b43d4c0130373dd2ddfc8f85059f12d4a843703a90c3970cfd17876844b4f3f48bf042bfa6b49f checksum: 10c0/6f10119d4570c7ba76dcf428abb8d3f69e63f92e51f700a542b43d4c0130373dd2ddfc8f85059f12d4a843703a90c3970cfd17876844b4f3f48bf042bfa6b49f
@ -4565,7 +4658,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"browserslist@npm:^4.24.0": "browserslist@npm:^4.21.1, browserslist@npm:^4.24.0":
version: 4.24.4 version: 4.24.4
resolution: "browserslist@npm:4.24.4" resolution: "browserslist@npm:4.24.4"
dependencies: dependencies:
@ -4798,6 +4891,21 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"cacheable-request@npm:^12.0.1":
version: 12.0.1
resolution: "cacheable-request@npm:12.0.1"
dependencies:
"@types/http-cache-semantics": "npm:^4.0.4"
get-stream: "npm:^9.0.1"
http-cache-semantics: "npm:^4.1.1"
keyv: "npm:^4.5.4"
mimic-response: "npm:^4.0.0"
normalize-url: "npm:^8.0.1"
responselike: "npm:^3.0.0"
checksum: 10c0/3ccc26519c8dd0821fcb21fa00781e55f05ab6e1da1487fbbee9c8c03435a3cf72c29a710a991cebe398fb9a5274e2a772fc488546d402db8dc21310764ed83a
languageName: node
linkType: hard
"cacheable-request@npm:^7.0.2": "cacheable-request@npm:^7.0.2":
version: 7.0.4 version: 7.0.4
resolution: "cacheable-request@npm:7.0.4" resolution: "cacheable-request@npm:7.0.4"
@ -4833,13 +4941,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"callsites@npm:^3.0.0": "callsites@npm:^3.0.0, callsites@npm:^3.1.0":
version: 3.1.0 version: 3.1.0
resolution: "callsites@npm:3.1.0" resolution: "callsites@npm:3.1.0"
checksum: 10c0/fff92277400eb06c3079f9e74f3af120db9f8ea03bad0e84d9aede54bbe2d44a56cccb5f6cf12211f93f52306df87077ecec5b712794c5a9b5dac6d615a3f301 checksum: 10c0/fff92277400eb06c3079f9e74f3af120db9f8ea03bad0e84d9aede54bbe2d44a56cccb5f6cf12211f93f52306df87077ecec5b712794c5a9b5dac6d615a3f301
languageName: node languageName: node
linkType: hard linkType: hard
"callsites@npm:^4.0.0":
version: 4.2.0
resolution: "callsites@npm:4.2.0"
checksum: 10c0/8f7e269ec09fc0946bb22d838a8bc7932e1909ab4a833b964749f4d0e8bdeaa1f253287c4f911f61781f09620b6925ccd19a5ea4897489c4e59442c660c312a3
languageName: node
linkType: hard
"camelcase@npm:5.0.0": "camelcase@npm:5.0.0":
version: 5.0.0 version: 5.0.0
resolution: "camelcase@npm:5.0.0" resolution: "camelcase@npm:5.0.0"
@ -5435,6 +5550,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"cssstyle@npm:^4.2.1":
version: 4.3.0
resolution: "cssstyle@npm:4.3.0"
dependencies:
"@asamuzakjp/css-color": "npm:^3.1.1"
rrweb-cssom: "npm:^0.8.0"
checksum: 10c0/770ccb288a99257fd0d5b129e03878f848e922d3b017358acb02e8dd530e8f0c7c6f74e6ae5367d715e2da36a490a734b4177fc1b78f3f08eca25f204a56a692
languageName: node
linkType: hard
"csstype@npm:3.1.3, csstype@npm:^3.0.2, csstype@npm:^3.1.3": "csstype@npm:3.1.3, csstype@npm:^3.0.2, csstype@npm:^3.1.3":
version: 3.1.3 version: 3.1.3
resolution: "csstype@npm:3.1.3" resolution: "csstype@npm:3.1.3"
@ -5563,6 +5688,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"data-urls@npm:^5.0.0":
version: 5.0.0
resolution: "data-urls@npm:5.0.0"
dependencies:
whatwg-mimetype: "npm:^4.0.0"
whatwg-url: "npm:^14.0.0"
checksum: 10c0/1b894d7d41c861f3a4ed2ae9b1c3f0909d4575ada02e36d3d3bc584bdd84278e20709070c79c3b3bff7ac98598cb191eb3e86a89a79ea4ee1ef360e1694f92ad
languageName: node
linkType: hard
"dayjs@npm:^1.11.11": "dayjs@npm:^1.11.11":
version: 1.11.13 version: 1.11.13
resolution: "dayjs@npm:1.11.13" resolution: "dayjs@npm:1.11.13"
@ -5628,6 +5763,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"decimal.js@npm:^10.4.3":
version: 10.5.0
resolution: "decimal.js@npm:10.5.0"
checksum: 10c0/785c35279df32762143914668df35948920b6c1c259b933e0519a69b7003fc0a5ed2a766b1e1dda02574450c566b21738a45f15e274b47c2ac02072c0d1f3ac3
languageName: node
linkType: hard
"decode-named-character-reference@npm:^1.0.0": "decode-named-character-reference@npm:^1.0.0":
version: 1.1.0 version: 1.1.0
resolution: "decode-named-character-reference@npm:1.1.0" resolution: "decode-named-character-reference@npm:1.1.0"
@ -5904,6 +6046,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"diff@npm:^7.0.0":
version: 7.0.0
resolution: "diff@npm:7.0.0"
checksum: 10c0/251fd15f85ffdf814cfc35a728d526b8d2ad3de338dcbd011ac6e57c461417090766b28995f8ff733135b5fbc3699c392db1d5e27711ac4e00244768cd1d577b
languageName: node
linkType: hard
"dingbat-to-unicode@npm:^1.0.1": "dingbat-to-unicode@npm:^1.0.1":
version: 1.0.1 version: 1.0.1
resolution: "dingbat-to-unicode@npm:1.0.1" resolution: "dingbat-to-unicode@npm:1.0.1"
@ -6034,6 +6183,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"dot-prop@npm:^7.2.0":
version: 7.2.0
resolution: "dot-prop@npm:7.2.0"
dependencies:
type-fest: "npm:^2.11.2"
checksum: 10c0/2621702a01e7a47730e3a8e2938a406afc79b62fbb77bd1394e786ff13776673904bf0a4fc6b812eb9849ec71034e9fc1019a9e0bbe91f84010d8a8088cd41a9
languageName: node
linkType: hard
"dotenv-cli@npm:^7.4.2": "dotenv-cli@npm:^7.4.2":
version: 7.4.4 version: 7.4.4
resolution: "dotenv-cli@npm:7.4.4" resolution: "dotenv-cli@npm:7.4.4"
@ -7516,7 +7674,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"form-data@npm:^4.0.0": "form-data-encoder@npm:^4.0.2":
version: 4.0.2
resolution: "form-data-encoder@npm:4.0.2"
checksum: 10c0/559d3130e265316452434eaf68d68560fb36392ff4d04614683419de4fb43c3dbe152dc303599fae382ce24d3451a6d3d289d3bcc182ae3d8ad32e7ce8e35e53
languageName: node
linkType: hard
"form-data@npm:^4.0.0, form-data@npm:^4.0.1":
version: 4.0.2 version: 4.0.2
resolution: "form-data@npm:4.0.2" resolution: "form-data@npm:4.0.2"
dependencies: dependencies:
@ -7758,6 +7923,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"generative-bayesian-network@npm:^2.1.63":
version: 2.1.63
resolution: "generative-bayesian-network@npm:2.1.63"
dependencies:
adm-zip: "npm:^0.5.9"
tslib: "npm:^2.4.0"
checksum: 10c0/d0b886663f14f7b8a43ea7f03fdac4e5d83f692a7829c5ed6af2ac6777b30b1c560dc1c55525bd7f50b0d0bf6ad28109151e85741b6f8dd4f0eb87f0dce42ea8
languageName: node
linkType: hard
"gensync@npm:^1.0.0-beta.2": "gensync@npm:^1.0.0-beta.2":
version: 1.0.0-beta.2 version: 1.0.0-beta.2
resolution: "gensync@npm:1.0.0-beta.2" resolution: "gensync@npm:1.0.0-beta.2"
@ -7847,6 +8022,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"get-stream@npm:^9.0.1":
version: 9.0.1
resolution: "get-stream@npm:9.0.1"
dependencies:
"@sec-ant/readable-stream": "npm:^0.4.1"
is-stream: "npm:^4.0.1"
checksum: 10c0/d70e73857f2eea1826ac570c3a912757dcfbe8a718a033fa0c23e12ac8e7d633195b01710e0559af574cbb5af101009b42df7b6f6b29ceec8dbdf7291931b948
languageName: node
linkType: hard
"get-uri@npm:^6.0.1": "get-uri@npm:^6.0.1":
version: 6.0.4 version: 6.0.4
resolution: "get-uri@npm:6.0.4" resolution: "get-uri@npm:6.0.4"
@ -8029,6 +8214,21 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"got-scraping@npm:^4.1.1":
version: 4.1.1
resolution: "got-scraping@npm:4.1.1"
dependencies:
got: "npm:^14.2.1"
header-generator: "npm:^2.1.41"
http2-wrapper: "npm:^2.2.0"
mimic-response: "npm:^4.0.0"
ow: "npm:^1.1.1"
quick-lru: "npm:^7.0.0"
tslib: "npm:^2.6.2"
checksum: 10c0/66b9bd88fea1c7a1248fec6e9c9757300b70e6039d2b2e0cf1c70e44e88be80f02a26e2e36d5f9c3acb4ec963558d72b0d236a7f11a7a6c87b39b5615afcf7db
languageName: node
linkType: hard
"got@npm:13.0.0": "got@npm:13.0.0":
version: 13.0.0 version: 13.0.0
resolution: "got@npm:13.0.0" resolution: "got@npm:13.0.0"
@ -8067,6 +8267,25 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"got@npm:^14.2.1":
version: 14.4.7
resolution: "got@npm:14.4.7"
dependencies:
"@sindresorhus/is": "npm:^7.0.1"
"@szmarczak/http-timer": "npm:^5.0.1"
cacheable-lookup: "npm:^7.0.0"
cacheable-request: "npm:^12.0.1"
decompress-response: "npm:^6.0.0"
form-data-encoder: "npm:^4.0.2"
http2-wrapper: "npm:^2.2.1"
lowercase-keys: "npm:^3.0.0"
p-cancelable: "npm:^4.0.1"
responselike: "npm:^3.0.0"
type-fest: "npm:^4.26.1"
checksum: 10c0/9b5b8dbc0642c78dbc64ab5ff6f12f6edab3e0cb80e89a3a69623a79ba3986f0ff0066a116fba47c0aacce4b0ba1eccf72f923f7fac13a31ce852bf9e2cb8f81
languageName: node
linkType: hard
"graceful-fs@npm:^4.1.10, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.1.9, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": "graceful-fs@npm:^4.1.10, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.1.9, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6":
version: 4.2.11 version: 4.2.11
resolution: "graceful-fs@npm:4.2.11" resolution: "graceful-fs@npm:4.2.11"
@ -8366,6 +8585,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"header-generator@npm:^2.1.41":
version: 2.1.63
resolution: "header-generator@npm:2.1.63"
dependencies:
browserslist: "npm:^4.21.1"
generative-bayesian-network: "npm:^2.1.63"
ow: "npm:^0.28.1"
tslib: "npm:^2.4.0"
checksum: 10c0/a6f49019d77df53dfaa0f6e4ad7f7b99a8ee5d598eeed5956d0adbd32a22846d2c7dfb4e912cdc0a06eb954b7c39a285bd1b329e479ae9d6f122e6c544425297
languageName: node
linkType: hard
"hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2": "hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2":
version: 3.3.2 version: 3.3.2
resolution: "hoist-non-react-statics@npm:3.3.2" resolution: "hoist-non-react-statics@npm:3.3.2"
@ -8398,6 +8629,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"html-encoding-sniffer@npm:^4.0.0":
version: 4.0.0
resolution: "html-encoding-sniffer@npm:4.0.0"
dependencies:
whatwg-encoding: "npm:^3.1.1"
checksum: 10c0/523398055dc61ac9b34718a719cb4aa691e4166f29187e211e1607de63dc25ac7af52ca7c9aead0c4b3c0415ffecb17326396e1202e2e86ff4bca4c0ee4c6140
languageName: node
linkType: hard
"html-parse-stringify@npm:^3.0.1": "html-parse-stringify@npm:^3.0.1":
version: 3.0.1 version: 3.0.1
resolution: "html-parse-stringify@npm:3.0.1" resolution: "html-parse-stringify@npm:3.0.1"
@ -8495,7 +8735,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.1": "http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.1, http-proxy-agent@npm:^7.0.2":
version: 7.0.2 version: 7.0.2
resolution: "http-proxy-agent@npm:7.0.2" resolution: "http-proxy-agent@npm:7.0.2"
dependencies: dependencies:
@ -8526,7 +8766,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"http2-wrapper@npm:^2.1.10": "http2-wrapper@npm:^2.1.10, http2-wrapper@npm:^2.2.0, http2-wrapper@npm:^2.2.1":
version: 2.2.1 version: 2.2.1
resolution: "http2-wrapper@npm:2.2.1" resolution: "http2-wrapper@npm:2.2.1"
dependencies: dependencies:
@ -9012,6 +9252,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-potential-custom-element-name@npm:^1.0.1":
version: 1.0.1
resolution: "is-potential-custom-element-name@npm:1.0.1"
checksum: 10c0/b73e2f22bc863b0939941d369486d308b43d7aef1f9439705e3582bfccaa4516406865e32c968a35f97a99396dac84e2624e67b0a16b0a15086a785e16ce7db9
languageName: node
linkType: hard
"is-promise@npm:^4.0.0": "is-promise@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "is-promise@npm:4.0.0" resolution: "is-promise@npm:4.0.0"
@ -9040,6 +9287,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-stream@npm:^4.0.1":
version: 4.0.1
resolution: "is-stream@npm:4.0.1"
checksum: 10c0/2706c7f19b851327ba374687bc4a3940805e14ca496dc672b9629e744d143b1ad9c6f1b162dece81c7bfbc0f83b32b61ccc19ad2e05aad2dd7af347408f60c7f
languageName: node
linkType: hard
"is-typedarray@npm:~1.0.0": "is-typedarray@npm:~1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "is-typedarray@npm:1.0.0" resolution: "is-typedarray@npm:1.0.0"
@ -9200,6 +9454,40 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"jsdom@npm:^26.0.0":
version: 26.0.0
resolution: "jsdom@npm:26.0.0"
dependencies:
cssstyle: "npm:^4.2.1"
data-urls: "npm:^5.0.0"
decimal.js: "npm:^10.4.3"
form-data: "npm:^4.0.1"
html-encoding-sniffer: "npm:^4.0.0"
http-proxy-agent: "npm:^7.0.2"
https-proxy-agent: "npm:^7.0.6"
is-potential-custom-element-name: "npm:^1.0.1"
nwsapi: "npm:^2.2.16"
parse5: "npm:^7.2.1"
rrweb-cssom: "npm:^0.8.0"
saxes: "npm:^6.0.0"
symbol-tree: "npm:^3.2.4"
tough-cookie: "npm:^5.0.0"
w3c-xmlserializer: "npm:^5.0.0"
webidl-conversions: "npm:^7.0.0"
whatwg-encoding: "npm:^3.1.1"
whatwg-mimetype: "npm:^4.0.0"
whatwg-url: "npm:^14.1.0"
ws: "npm:^8.18.0"
xml-name-validator: "npm:^5.0.0"
peerDependencies:
canvas: ^3.0.0
peerDependenciesMeta:
canvas:
optional: true
checksum: 10c0/e48725ba4027edcfc9bca5799eaec72c6561ecffe3675a8ff87fe9c3541ca4ff9f82b4eff5b3d9c527302da0d859b2f60e9364347a5d42b77f5c76c436c569dc
languageName: node
linkType: hard
"jsesc@npm:^3.0.2": "jsesc@npm:^3.0.2":
version: 3.1.0 version: 3.1.0
resolution: "jsesc@npm:3.1.0" resolution: "jsesc@npm:3.1.0"
@ -9801,7 +10089,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": "lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0, lru-cache@npm:^10.4.3":
version: 10.4.3 version: 10.4.3
resolution: "lru-cache@npm:10.4.3" resolution: "lru-cache@npm:10.4.3"
checksum: 10c0/ebd04fbca961e6c1d6c0af3799adcc966a1babe798f685bb84e6599266599cd95d94630b10262f5424539bc4640107e8a33aa28585374abf561d30d16f4b39fb checksum: 10c0/ebd04fbca961e6c1d6c0af3799adcc966a1babe798f685bb84e6599266599cd95d94630b10262f5424539bc4640107e8a33aa28585374abf561d30d16f4b39fb
@ -11533,7 +11821,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"normalize-url@npm:^8.0.0": "normalize-url@npm:^8.0.0, normalize-url@npm:^8.0.1":
version: 8.0.1 version: 8.0.1
resolution: "normalize-url@npm:8.0.1" resolution: "normalize-url@npm:8.0.1"
checksum: 10c0/eb439231c4b84430f187530e6fdac605c5048ef4ec556447a10c00a91fc69b52d8d8298d9d608e68d3e0f7dc2d812d3455edf425e0f215993667c3183bcab1ef checksum: 10c0/eb439231c4b84430f187530e6fdac605c5048ef4ec556447a10c00a91fc69b52d8d8298d9d608e68d3e0f7dc2d812d3455edf425e0f215993667c3183bcab1ef
@ -11614,6 +11902,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"nwsapi@npm:^2.2.16":
version: 2.2.20
resolution: "nwsapi@npm:2.2.20"
checksum: 10c0/07f4dafa3186aef7c007863e90acd4342a34ba9d44b22f14f644fdb311f6086887e21c2fc15efaa826c2bc39ab2bc841364a1a630e7c87e0cb723ba59d729297
languageName: node
linkType: hard
"oauth-sign@npm:~0.9.0": "oauth-sign@npm:~0.9.0":
version: 0.9.0 version: 0.9.0
resolution: "oauth-sign@npm:0.9.0" resolution: "oauth-sign@npm:0.9.0"
@ -11887,6 +12182,32 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ow@npm:^0.28.1":
version: 0.28.2
resolution: "ow@npm:0.28.2"
dependencies:
"@sindresorhus/is": "npm:^4.2.0"
callsites: "npm:^3.1.0"
dot-prop: "npm:^6.0.1"
lodash.isequal: "npm:^4.5.0"
vali-date: "npm:^1.0.0"
checksum: 10c0/8d0de10fd3aa1ab69dd844ace087718c31ceb1a25cf79d38a5be4d0a5da46f960b6bc15a95405747899b882fb51dcf5a502d7e6508005d1c57e157d12fa17cdd
languageName: node
linkType: hard
"ow@npm:^1.1.1":
version: 1.1.1
resolution: "ow@npm:1.1.1"
dependencies:
"@sindresorhus/is": "npm:^5.3.0"
callsites: "npm:^4.0.0"
dot-prop: "npm:^7.2.0"
lodash.isequal: "npm:^4.5.0"
vali-date: "npm:^1.0.0"
checksum: 10c0/3973f9d6245f2e468a0f1d614ece96f1289632f7425094e8b266b50ddbe79471f2e6cba447b80e90b54bbeb13c20e83671edfb5ef4c0b13c15546ba0710554e1
languageName: node
linkType: hard
"p-cancelable@npm:^2.0.0": "p-cancelable@npm:^2.0.0":
version: 2.1.1 version: 2.1.1
resolution: "p-cancelable@npm:2.1.1" resolution: "p-cancelable@npm:2.1.1"
@ -11901,6 +12222,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"p-cancelable@npm:^4.0.1":
version: 4.0.1
resolution: "p-cancelable@npm:4.0.1"
checksum: 10c0/12636623f46784ba962b6fe7a1f34d021f1d9a2cc12c43e270baa715ea872d5c8c7d9f086ed420b8b9817e91d9bbe92c14c90e5dddd4a9968c81a2a7aef7089d
languageName: node
linkType: hard
"p-finally@npm:^1.0.0": "p-finally@npm:^1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "p-finally@npm:1.0.0" resolution: "p-finally@npm:1.0.0"
@ -12138,7 +12466,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"parse5@npm:^7.0.0": "parse5@npm:^7.0.0, parse5@npm:^7.2.1":
version: 7.2.1 version: 7.2.1
resolution: "parse5@npm:7.2.1" resolution: "parse5@npm:7.2.1"
dependencies: dependencies:
@ -12734,6 +13062,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"quick-lru@npm:^7.0.0":
version: 7.0.0
resolution: "quick-lru@npm:7.0.0"
checksum: 10c0/d46e3dc6d2f79f83d3dc852b32b3d9fcf5337bf5b4016122c1b729aa7bcd54dd79b2f3cb36b59a5a3f5c30443ecfbc9e7d400cfc30feedd8d58d3d4213d08a18
languageName: node
linkType: hard
"raf-schd@npm:^4.0.3": "raf-schd@npm:^4.0.3":
version: 4.0.3 version: 4.0.3
resolution: "raf-schd@npm:4.0.3" resolution: "raf-schd@npm:4.0.3"
@ -14139,6 +14474,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"rrweb-cssom@npm:^0.8.0":
version: 0.8.0
resolution: "rrweb-cssom@npm:0.8.0"
checksum: 10c0/56f2bfd56733adb92c0b56e274c43f864b8dd48784d6fe946ef5ff8d438234015e59ad837fc2ad54714b6421384141c1add4eb569e72054e350d1f8a50b8ac7b
languageName: node
linkType: hard
"run-parallel@npm:^1.1.9": "run-parallel@npm:^1.1.9":
version: 1.2.0 version: 1.2.0
resolution: "run-parallel@npm:1.2.0" resolution: "run-parallel@npm:1.2.0"
@ -14202,6 +14544,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"saxes@npm:^6.0.0":
version: 6.0.0
resolution: "saxes@npm:6.0.0"
dependencies:
xmlchars: "npm:^2.2.0"
checksum: 10c0/3847b839f060ef3476eb8623d099aa502ad658f5c40fd60c105ebce86d244389b0d76fcae30f4d0c728d7705ceb2f7e9b34bb54717b6a7dbedaf5dad2d9a4b74
languageName: node
linkType: hard
"scheduler@npm:^0.25.0": "scheduler@npm:^0.25.0":
version: 0.25.0 version: 0.25.0
resolution: "scheduler@npm:0.25.0" resolution: "scheduler@npm:0.25.0"
@ -15045,6 +15396,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"symbol-tree@npm:^3.2.4":
version: 3.2.4
resolution: "symbol-tree@npm:3.2.4"
checksum: 10c0/dfbe201ae09ac6053d163578778c53aa860a784147ecf95705de0cd23f42c851e1be7889241495e95c37cabb058edb1052f141387bef68f705afc8f9dd358509
languageName: node
linkType: hard
"synckit@npm:^0.9.1": "synckit@npm:^0.9.1":
version: 0.9.2 version: 0.9.2
resolution: "synckit@npm:0.9.2" resolution: "synckit@npm:0.9.2"
@ -15220,6 +15578,24 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tldts-core@npm:^6.1.85":
version: 6.1.85
resolution: "tldts-core@npm:6.1.85"
checksum: 10c0/f028759b361bef86d3dd8dbaaa010b4c3aca236ec7ba097658b9a595243bb34c98206e410cc3631055af6ed34012f5da7e9af2c4e38e218d5fc693df6ab3da33
languageName: node
linkType: hard
"tldts@npm:^6.1.32":
version: 6.1.85
resolution: "tldts@npm:6.1.85"
dependencies:
tldts-core: "npm:^6.1.85"
bin:
tldts: bin/cli.js
checksum: 10c0/83bc222046f36a9ca071b662e3272bb1e98e849fa4bddfab0a3eba8804a4a539b02bcbe55e1277fe713de6677e55104da2ef9a54d006c642b6e9f26c7595d5aa
languageName: node
linkType: hard
"tmp-promise@npm:^3.0.2": "tmp-promise@npm:^3.0.2":
version: 3.0.3 version: 3.0.3
resolution: "tmp-promise@npm:3.0.3" resolution: "tmp-promise@npm:3.0.3"
@ -15293,6 +15669,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tough-cookie@npm:^5.0.0":
version: 5.1.2
resolution: "tough-cookie@npm:5.1.2"
dependencies:
tldts: "npm:^6.1.32"
checksum: 10c0/5f95023a47de0f30a902bba951664b359725597d8adeabc66a0b93a931c3af801e1e697dae4b8c21a012056c0ea88bd2bf4dfe66b2adcf8e2f42cd9796fe0626
languageName: node
linkType: hard
"tough-cookie@npm:~2.5.0": "tough-cookie@npm:~2.5.0":
version: 2.5.0 version: 2.5.0
resolution: "tough-cookie@npm:2.5.0" resolution: "tough-cookie@npm:2.5.0"
@ -15303,6 +15688,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tr46@npm:^5.1.0":
version: 5.1.0
resolution: "tr46@npm:5.1.0"
dependencies:
punycode: "npm:^2.3.1"
checksum: 10c0/d761f7144e0cb296187674ef245c74f761e334d7cf25ca73ef60e4c72c097c75051031c093fa1a2fee04b904977b316716a7915854bcae8fb1a371746513cbe8
languageName: node
linkType: hard
"tr46@npm:~0.0.3": "tr46@npm:~0.0.3":
version: 0.0.3 version: 0.0.3
resolution: "tr46@npm:0.0.3" resolution: "tr46@npm:0.0.3"
@ -15363,7 +15757,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.6.2": "tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2":
version: 2.8.1 version: 2.8.1
resolution: "tslib@npm:2.8.1" resolution: "tslib@npm:2.8.1"
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
@ -15379,6 +15773,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"turndown-plugin-gfm@npm:^1.0.2":
version: 1.0.2
resolution: "turndown-plugin-gfm@npm:1.0.2"
checksum: 10c0/eb9bc20dbb08d5335231f9617d7440f14b35781f14a3a393d8f13fc8205afeb11a0a632d52da4548ab0fa353f315ca265462b24d368faf23258dccbe439182b9
languageName: node
linkType: hard
"turndown@npm:^7.2.0":
version: 7.2.0
resolution: "turndown@npm:7.2.0"
dependencies:
"@mixmark-io/domino": "npm:^2.2.0"
checksum: 10c0/6abcdcdf9d35cd79d7a8100a7de1d2226b921d5bd99e73ac14a7ead39c059978f519378913375efb04c68bcfc40f7ffe2dee0ce9ae4d54dc1235b12856a78d4e
languageName: node
linkType: hard
"tweetnacl@npm:^0.14.3, tweetnacl@npm:~0.14.0": "tweetnacl@npm:^0.14.3, tweetnacl@npm:~0.14.0":
version: 0.14.5 version: 0.14.5
resolution: "tweetnacl@npm:0.14.5" resolution: "tweetnacl@npm:0.14.5"
@ -15402,13 +15812,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"type-fest@npm:^2.17.0": "type-fest@npm:^2.11.2, type-fest@npm:^2.17.0":
version: 2.19.0 version: 2.19.0
resolution: "type-fest@npm:2.19.0" resolution: "type-fest@npm:2.19.0"
checksum: 10c0/a5a7ecf2e654251613218c215c7493574594951c08e52ab9881c9df6a6da0aeca7528c213c622bc374b4e0cb5c443aa3ab758da4e3c959783ce884c3194e12cb checksum: 10c0/a5a7ecf2e654251613218c215c7493574594951c08e52ab9881c9df6a6da0aeca7528c213c622bc374b4e0cb5c443aa3ab758da4e3c959783ce884c3194e12cb
languageName: node languageName: node
linkType: hard linkType: hard
"type-fest@npm:^4.26.1":
version: 4.39.1
resolution: "type-fest@npm:4.39.1"
checksum: 10c0/f5bf302eb2e2f70658be1757aa578f4a09da3f65699b0b12b7ae5502ccea76e5124521a6e6b69540f442c3dc924c394202a2ab58718d0582725c7ac23c072594
languageName: node
linkType: hard
"type-fest@npm:^4.37.0": "type-fest@npm:^4.37.0":
version: 4.37.0 version: 4.37.0
resolution: "type-fest@npm:4.37.0" resolution: "type-fest@npm:4.37.0"
@ -15860,6 +16277,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"vali-date@npm:^1.0.0":
version: 1.0.0
resolution: "vali-date@npm:1.0.0"
checksum: 10c0/5755215f6734caab535f60af0a32bbbf2052c61b1a40668d773df78fd3754e4fe9da2ea5466731505f3e0a599acc209d5578c4b70488ed120fb03f0c2ab06449
languageName: node
linkType: hard
"validate-npm-package-license@npm:^3.0.1": "validate-npm-package-license@npm:^3.0.1":
version: 3.0.4 version: 3.0.4
resolution: "validate-npm-package-license@npm:3.0.4" resolution: "validate-npm-package-license@npm:3.0.4"
@ -16001,6 +16425,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"w3c-xmlserializer@npm:^5.0.0":
version: 5.0.0
resolution: "w3c-xmlserializer@npm:5.0.0"
dependencies:
xml-name-validator: "npm:^5.0.0"
checksum: 10c0/8712774c1aeb62dec22928bf1cdfd11426c2c9383a1a63f2bcae18db87ca574165a0fbe96b312b73652149167ac6c7f4cf5409f2eb101d9c805efe0e4bae798b
languageName: node
linkType: hard
"web-namespaces@npm:^2.0.0": "web-namespaces@npm:^2.0.0":
version: 2.0.1 version: 2.0.1
resolution: "web-namespaces@npm:2.0.1" resolution: "web-namespaces@npm:2.0.1"
@ -16051,6 +16484,39 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"webidl-conversions@npm:^7.0.0":
version: 7.0.0
resolution: "webidl-conversions@npm:7.0.0"
checksum: 10c0/228d8cb6d270c23b0720cb2d95c579202db3aaf8f633b4e9dd94ec2000a04e7e6e43b76a94509cdb30479bd00ae253ab2371a2da9f81446cc313f89a4213a2c4
languageName: node
linkType: hard
"whatwg-encoding@npm:^3.1.1":
version: 3.1.1
resolution: "whatwg-encoding@npm:3.1.1"
dependencies:
iconv-lite: "npm:0.6.3"
checksum: 10c0/273b5f441c2f7fda3368a496c3009edbaa5e43b71b09728f90425e7f487e5cef9eb2b846a31bd760dd8077739c26faf6b5ca43a5f24033172b003b72cf61a93e
languageName: node
linkType: hard
"whatwg-mimetype@npm:^4.0.0":
version: 4.0.0
resolution: "whatwg-mimetype@npm:4.0.0"
checksum: 10c0/a773cdc8126b514d790bdae7052e8bf242970cebd84af62fb2f35a33411e78e981f6c0ab9ed1fe6ec5071b09d5340ac9178e05b52d35a9c4bcf558ba1b1551df
languageName: node
linkType: hard
"whatwg-url@npm:^14.0.0, whatwg-url@npm:^14.1.0":
version: 14.2.0
resolution: "whatwg-url@npm:14.2.0"
dependencies:
tr46: "npm:^5.1.0"
webidl-conversions: "npm:^7.0.0"
checksum: 10c0/f746fc2f4c906607d09537de1227b13f9494c171141e5427ed7d2c0dd0b6a48b43d8e71abaae57d368d0c06b673fd8ec63550b32ad5ed64990c7b0266c2b4272
languageName: node
linkType: hard
"whatwg-url@npm:^5.0.0": "whatwg-url@npm:^5.0.0":
version: 5.0.0 version: 5.0.0
resolution: "whatwg-url@npm:5.0.0" resolution: "whatwg-url@npm:5.0.0"
@ -16221,6 +16687,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"xml-name-validator@npm:^5.0.0":
version: 5.0.0
resolution: "xml-name-validator@npm:5.0.0"
checksum: 10c0/3fcf44e7b73fb18be917fdd4ccffff3639373c7cb83f8fc35df6001fecba7942f1dbead29d91ebb8315e2f2ff786b508f0c9dc0215b6353f9983c6b7d62cb1f5
languageName: node
linkType: hard
"xml-parse-from-string@npm:^1.0.0": "xml-parse-from-string@npm:^1.0.0":
version: 1.0.1 version: 1.0.1
resolution: "xml-parse-from-string@npm:1.0.1" resolution: "xml-parse-from-string@npm:1.0.1"
@ -16286,6 +16759,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"xmlchars@npm:^2.2.0":
version: 2.2.0
resolution: "xmlchars@npm:2.2.0"
checksum: 10c0/b64b535861a6f310c5d9bfa10834cf49127c71922c297da9d4d1b45eeaae40bf9b4363275876088fbe2667e5db028d2cd4f8ee72eed9bede840a67d57dab7593
languageName: node
linkType: hard
"xmldom-sre@npm:0.1.31": "xmldom-sre@npm:0.1.31":
version: 0.1.31 version: 0.1.31
resolution: "xmldom-sre@npm:0.1.31" resolution: "xmldom-sre@npm:0.1.31"