mirror of
https://github.com/purocean/yn.git
synced 2026-08-29 01:21:05 +08:00
Merge remote-tracking branch 'origin/copilot/add-config-retrieval-tools' into develop
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user