Merge remote-tracking branch 'origin/copilot/add-config-retrieval-tools' into develop

This commit is contained in:
purocean
2026-05-16 10:43:06 +08:00
5 changed files with 279 additions and 1 deletions
+3
View File
@@ -4,6 +4,9 @@ import { JSONRPCClient, JSONRPCClientChannel, JSONRPCError, JSONRPCRequest, JSON
type Ctx = {
setting: {
showSettingPanel: (key?: string) => void
getSchemaForMcp: () => Promise<any>
getSettingsForMcp: () => Promise<Record<string, any>>
setSettingForMcp: (key: string, value: any) => Promise<Record<string, any>>
},
doc: {
switchDocByPath: (path: string) => Promise<void>
+60
View File
@@ -14,6 +14,11 @@ const mocks = vi.hoisted(() => ({
getRawActions: vi.fn(),
executeAction: vi.fn(),
},
setting: {
getSchemaForMcp: vi.fn(),
getSettingsForMcp: vi.fn(),
setSettingForMcp: vi.fn(),
},
},
},
},
@@ -115,6 +120,9 @@ afterEach(() => {
mocks.exportDocumentForMcp.mockReset()
mocks.jsonRPCClient.call.ctx.action.getRawActions.mockReset()
mocks.jsonRPCClient.call.ctx.action.executeAction.mockReset()
mocks.jsonRPCClient.call.ctx.setting.getSchemaForMcp.mockReset()
mocks.jsonRPCClient.call.ctx.setting.getSettingsForMcp.mockReset()
mocks.jsonRPCClient.call.ctx.setting.setSettingForMcp.mockReset()
})
describe('MCP server request handling', () => {
@@ -153,6 +161,9 @@ describe('MCP server request handling', () => {
'yn_get_markdown_features_doc',
'yn_reload_main_window',
'yn_export_document',
'yn_get_config_schema',
'yn_get_all_configs',
'yn_set_config',
])
const callTool = server.handlers.get(CallToolRequestSchema)
@@ -207,6 +218,55 @@ describe('MCP server request handling', () => {
expect(JSON.parse(exported.content[0].text)).toEqual({ success: true, result: { base64: 'ZGF0YQ==' } })
})
it('gets config schema, fetches all configs, and sets a config with refresh', async () => {
const { server } = await initEnabledServer()
const callTool = server.handlers.get(CallToolRequestSchema)
mocks.jsonRPCClient.call.ctx.setting.getSchemaForMcp.mockResolvedValue({
properties: {
theme: {
type: 'string',
title: 'Theme',
defaultValue: 'system',
enum: ['system', 'dark', 'light'],
},
},
})
const schema = await callTool({ params: { name: 'yn_get_config_schema', arguments: { key: 'theme' } } })
expect(JSON.parse(schema.content[0].text)).toEqual({
success: true,
result: {
key: 'theme',
config: {
type: 'string',
title: 'Theme',
defaultValue: 'system',
enum: ['system', 'dark', 'light'],
},
},
})
mocks.jsonRPCClient.call.ctx.setting.getSettingsForMcp.mockResolvedValue({ theme: 'dark', readonly: true })
const allConfigs = await callTool({ params: { name: 'yn_get_all_configs', arguments: {} } })
expect(JSON.parse(allConfigs.content[0].text)).toEqual({
success: true,
result: { theme: 'dark', readonly: true },
})
mocks.jsonRPCClient.call.ctx.setting.setSettingForMcp.mockResolvedValue({ theme: 'light' })
const setConfig = await callTool({ params: { name: 'yn_set_config', arguments: { key: 'theme', value: 'light' } } })
expect(JSON.parse(setConfig.content[0].text)).toEqual({
success: true,
result: { key: 'theme', value: 'light', refreshed: true },
})
expect(mocks.jsonRPCClient.call.ctx.setting.setSettingForMcp).toHaveBeenCalledWith('theme', 'light')
mocks.jsonRPCClient.call.ctx.setting.getSchemaForMcp.mockResolvedValue({ properties: {} })
const missing = await callTool({ params: { name: 'yn_get_config_schema', arguments: { key: 'missing' } } })
expect(missing.isError).toBe(true)
expect(JSON.parse(missing.content[0].text)).toEqual({ success: false, error: 'Unknown config key: missing' })
})
it('handles transport errors and unknown tools', async () => {
mocks.enabled = true
const { handleMCPRequest } = await import('../mcp')
+149
View File
@@ -42,6 +42,35 @@ async function executeAction (actionName: string, args: any[]): Promise<any> {
return await jsonRPCClient.call.ctx.action.executeAction(actionName, ...args)
}
async function getConfigSchema (key?: string) {
const schema = await jsonRPCClient.call.ctx.setting.getSchemaForMcp()
if (!key) {
return schema
}
const config = schema?.properties?.[key]
if (!config) {
throw new Error(`Unknown config key: ${key}`)
}
return { key, config }
}
async function getAllConfigs () {
return await jsonRPCClient.call.ctx.setting.getSettingsForMcp()
}
async function setConfig (key: string, value: any) {
const settings = await jsonRPCClient.call.ctx.setting.setSettingForMcp(key, value)
return {
key,
value: settings?.[key],
refreshed: true,
}
}
/**
* Get the built-in documentation for Yank Note's extended Markdown features.
*/
@@ -393,6 +422,47 @@ function createMCPServer (): Server {
],
},
},
{
name: 'yn_get_config_schema',
description: 'Get Yank Note config schema definitions, including title, description, type, default value, enum options, and dynamically updated schema fields from frontend plugins.',
inputSchema: {
type: 'object',
properties: {
key: {
type: 'string',
description: 'Optional config key. Omit to return the full config schema.',
},
},
additionalProperties: false,
},
},
{
name: 'yn_get_all_configs',
description: 'Get all current Yank Note config values after refreshing settings from the frontend.',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: false,
},
},
{
name: 'yn_set_config',
description: 'Set one Yank Note config value by key, persist it, and refresh frontend settings/hooks immediately.',
inputSchema: {
type: 'object',
properties: {
key: {
type: 'string',
description: 'Config key to update.',
},
value: {
description: 'New config value. Match the type defined in yn_get_config_schema.',
},
},
required: ['key', 'value'],
additionalProperties: false,
},
},
],
}
})
@@ -525,6 +595,85 @@ function createMCPServer (): Server {
}
}
if (name === 'yn_get_config_schema') {
const { key } = (args || {}) as any
try {
const result = await getConfigSchema(key)
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: true, result }),
},
],
}
} catch (error: any) {
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: false, error: error.message }),
},
],
isError: true,
}
}
}
if (name === 'yn_get_all_configs') {
try {
const result = await getAllConfigs()
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: true, result }),
},
],
}
} catch (error: any) {
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: false, error: error.message }),
},
],
isError: true,
}
}
}
if (name === 'yn_set_config') {
const { key, value } = (args || {}) as any
try {
const result = await setConfig(key, value)
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: true, result }),
},
],
}
} catch (error: any) {
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: false, error: error.message }),
},
],
isError: true,
}
}
}
throw new Error(`Unknown tool: ${name}`)
})
@@ -5,6 +5,7 @@ const mocks = vi.hoisted(() => ({
registeredHooks: [] as any[],
showSetting: false,
themeName: 'dark',
setTheme: vi.fn(),
}))
vi.mock('@fe/others/setting-schema', () => ({
@@ -63,6 +64,7 @@ vi.mock('@fe/services/i18n', () => ({
vi.mock('../theme', () => ({
getThemeName: () => mocks.themeName,
setTheme: mocks.setTheme,
}))
async function importSetting (initSettings?: any) {
@@ -79,6 +81,7 @@ beforeEach(() => {
mocks.registeredHooks = []
mocks.showSetting = false
mocks.themeName = 'dark'
mocks.setTheme.mockReset()
document.body.innerHTML = ''
delete (window as any)._INIT_SETTINGS
})
@@ -196,6 +199,41 @@ test('gets and sets individual settings without exposing mutable local state', a
expect(mocks.writtenSettings).toStrictEqual({ readonly: true })
})
test('provides MCP helpers for schema, refresh, and setting updates', async () => {
const setting = await importSetting({ readonly: false })
mocks.fetchedSettings = { readonly: true }
const schema = await setting.getSchemaForMcp()
expect(schema.properties.readonly).toBeTruthy()
expect(mocks.hooks[0]).toStrictEqual({
name: 'SETTING_PANEL_BEFORE_SHOW',
payload: {},
options: { breakable: true },
})
mocks.hooks = []
const settings = await setting.getSettingsForMcp()
expect(settings.readonly).toBe(true)
expect(mocks.hooks[0]).toMatchObject({ name: 'SETTING_FETCHED' })
mocks.hooks = []
await setting.setSettingForMcp('readonly', false)
expect(mocks.hooks[0]).toStrictEqual({
name: 'SETTING_PANEL_BEFORE_SHOW',
payload: {},
options: { breakable: true },
})
expect(mocks.writtenSettings).toStrictEqual({ readonly: false })
mocks.hooks = []
mocks.fetchedSettings = { readonly: true }
const themeSettings = await setting.setSettingForMcp('theme', 'light')
expect(mocks.setTheme).toHaveBeenCalledWith('light')
expect(themeSettings.readonly).toBe(true)
await expect(setting.setSettingForMcp('missing', true)).rejects.toThrow('Unknown setting key: missing')
})
test('shows, locates, and hides setting panel', async () => {
const setting = await importSetting()
const tab = document.createElement('div')
+29 -1
View File
@@ -6,7 +6,7 @@ import { basename } from '@fe/utils/path'
import { sleep } from '@fe/utils'
import type { BuildInSettings, FileItem, PathItem, SettingGroup, SettingSchema } from '@fe/types'
import { getDefaultSettingSchema } from '@fe/others/setting-schema'
import { getThemeName } from './theme'
import { getThemeName, setTheme } from './theme'
import { t } from './i18n'
type Schema = SettingSchema
@@ -44,6 +44,10 @@ export function changeSchema (fun: (schema: Schema) => void) {
fun(schema)
}
async function prepareSchemaForMcp () {
await triggerHook('SETTING_PANEL_BEFORE_SHOW', {}, { breakable: true })
}
function transformSettings (data: any) {
if (!data) {
return {}
@@ -149,6 +153,15 @@ export function getSettings () {
return cloneDeep(settings)
}
export async function getSchemaForMcp () {
await prepareSchemaForMcp()
return getSchema()
}
export async function getSettingsForMcp () {
return await fetchSettings()
}
/**
* get setting val by key
* @param key
@@ -175,6 +188,21 @@ export async function setSetting<T extends keyof BuildInSettings> (key: T, val:
await writeSettings({ [key]: val })
}
export async function setSettingForMcp (key: string, val: any) {
await prepareSchemaForMcp()
if (!Object.prototype.hasOwnProperty.call(schema.properties, key)) {
throw new Error(`Unknown setting key: ${key}`)
}
if (key === 'theme') {
setTheme(val)
return await fetchSettings()
}
return await writeSettings({ [key]: val })
}
/**
* Show setting panel.
* @param keyOrGroup