fix(mcp): align runtime metadata and restart errors
- Publish explicit effective tool state while retaining disabled tools for Settings - Classify filesystem writes as side effects and localize category labels - Stop failed restarts and return precise development control status codes
This commit is contained in:
parent
7955207146
commit
fad731b3e7
|
|
@ -10,7 +10,7 @@
|
|||
- Run Pi through AI SDK HarnessAgent as a configurable desktop provider with multiple saved model profiles, secure credentials, existing MCP design tools, and per-profile thinking and permission settings.
|
||||
- Open multiple selected design files in separate tabs.
|
||||
- Let Figma API scripts and automation combine components into variant sets.
|
||||
- Monitor and restart the local MCP server, configure its root directory or authentication, and choose individual, read-only, or document-writing tools it exposes from Settings.
|
||||
- Monitor and restart the local MCP server, configure its root directory or authentication, and choose individual, read-only, or side-effecting tools it exposes from Settings.
|
||||
- Add deterministic two-browser collaboration coverage for bidirectional edits, awareness, departure cleanup, partitioned-peer convergence, and reconnect synchronization without public network dependencies. (#530)
|
||||
- Import, render, edit, resize, select, and export Figma text-on-path layers while preserving their curved glyph layout.
|
||||
- Show Figma-style temporary distance measurements between selected and Option/Alt-hovered layers. (#491)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { preprocessRPC } from '#mcp/jsx-preprocess'
|
|||
import { createMCPSessionManager } from '#mcp/server/sessions'
|
||||
import { createToolDescriptors } from '#mcp/tool/manifest'
|
||||
import type { ToolDescriptor, ToolPolicy } from '#mcp/tool/metadata'
|
||||
import { applyToolPolicy } from '#mcp/tool/policy'
|
||||
import { registerTools } from '#mcp/tool/registration'
|
||||
|
||||
import packageJSON from '../package.json' with { type: 'json' }
|
||||
|
|
@ -317,7 +318,7 @@ function buildServerContext(options: ServerOptions) {
|
|||
onConnectionChange: mcpSessions.notifyToolsChanged
|
||||
})
|
||||
const sendToBrowser = browserRPC.sendRPC
|
||||
const toolDescriptors = createToolDescriptors(mcpRoot !== null)
|
||||
const toolDescriptors = applyToolPolicy(createToolDescriptors(mcpRoot !== null), toolPolicy)
|
||||
|
||||
const app = createHonoApp({
|
||||
authToken,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ function coreToolDescriptor(def: ToolDef): ToolDescriptor {
|
|||
description: def.description,
|
||||
effect: coreToolEffect(def),
|
||||
availability: coreToolAvailability(def),
|
||||
capabilities: coreToolCapabilities(def)
|
||||
capabilities: coreToolCapabilities(def),
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -49,15 +50,17 @@ export function createToolDescriptors(filesystemEnabled: boolean): ToolDescripto
|
|||
'List open OpenPencil documents/tabs with their IDs, file paths, current pages, and pages.',
|
||||
effect: 'read',
|
||||
availability: 'default',
|
||||
capabilities: ['document:read']
|
||||
capabilities: ['document:read'],
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
name: 'save_file',
|
||||
description:
|
||||
'Save the current document to disk. An optional path must stay inside the configured MCP root.',
|
||||
effect: 'read',
|
||||
effect: 'write',
|
||||
availability: 'default',
|
||||
capabilities: ['document:read', 'filesystem:write']
|
||||
capabilities: ['document:read', 'filesystem:write'],
|
||||
enabled: true
|
||||
},
|
||||
...(filesystemEnabled
|
||||
? [
|
||||
|
|
@ -66,7 +69,8 @@ export function createToolDescriptors(filesystemEnabled: boolean): ToolDescripto
|
|||
description: 'Open a .fig or .pen file from inside the configured MCP root.',
|
||||
effect: 'write',
|
||||
availability: 'filesystem',
|
||||
capabilities: ['filesystem:read', 'document:write']
|
||||
capabilities: ['filesystem:read', 'document:write'],
|
||||
enabled: true
|
||||
} satisfies ToolDescriptor,
|
||||
{
|
||||
name: 'new_document',
|
||||
|
|
@ -74,7 +78,8 @@ export function createToolDescriptors(filesystemEnabled: boolean): ToolDescripto
|
|||
'Create a new empty document with an optional save path inside the configured MCP root.',
|
||||
effect: 'write',
|
||||
availability: 'filesystem',
|
||||
capabilities: ['document:write', 'filesystem:write']
|
||||
capabilities: ['document:write', 'filesystem:write'],
|
||||
enabled: true
|
||||
} satisfies ToolDescriptor
|
||||
]
|
||||
: []),
|
||||
|
|
@ -84,7 +89,8 @@ export function createToolDescriptors(filesystemEnabled: boolean): ToolDescripto
|
|||
'Get design-to-code generation guidelines. Call before generating frontend code.',
|
||||
effect: 'read',
|
||||
availability: 'default',
|
||||
capabilities: []
|
||||
capabilities: [],
|
||||
enabled: true
|
||||
}
|
||||
)
|
||||
return descriptors
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export interface ToolDescriptor {
|
|||
effect: ToolEffect
|
||||
availability: ToolAvailability
|
||||
capabilities: ToolCapability[]
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface ToolPolicy {
|
||||
|
|
@ -42,11 +43,12 @@ const TOOL_CAPABILITIES: ReadonlySet<string> = new Set<ToolCapability>([
|
|||
|
||||
export function parseToolDescriptor(value: unknown): ToolDescriptor | null {
|
||||
if (!isRecord(value)) return null
|
||||
const { name, description, effect, availability, capabilities } = value
|
||||
const { name, description, effect, availability, capabilities, enabled } = value
|
||||
if (typeof name !== 'string' || !name) return null
|
||||
if (typeof description !== 'string') return null
|
||||
if (typeof effect !== 'string' || !TOOL_EFFECTS.has(effect)) return null
|
||||
if (typeof availability !== 'string' || !TOOL_AVAILABILITIES.has(availability)) return null
|
||||
if (typeof enabled !== 'boolean') return null
|
||||
if (
|
||||
!Array.isArray(capabilities) ||
|
||||
capabilities.some(
|
||||
|
|
@ -60,6 +62,7 @@ export function parseToolDescriptor(value: unknown): ToolDescriptor | null {
|
|||
description,
|
||||
effect: effect as ToolEffect,
|
||||
availability: availability as ToolAvailability,
|
||||
capabilities: capabilities as ToolCapability[]
|
||||
capabilities: capabilities as ToolCapability[],
|
||||
enabled
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ToolPolicy } from '#mcp/tool/metadata'
|
||||
import type { ToolDescriptor, ToolPolicy } from '#mcp/tool/metadata'
|
||||
|
||||
export function parseDisabledTools(value: string | undefined): string[] {
|
||||
if (!value) return []
|
||||
|
|
@ -12,6 +12,21 @@ export function parseDisabledTools(value: string | undefined): string[] {
|
|||
]
|
||||
}
|
||||
|
||||
export function isToolEnabled(descriptor: ToolDescriptor, policy: ToolPolicy): boolean {
|
||||
if (policy.disabledTools.includes(descriptor.name)) return false
|
||||
return descriptor.availability !== 'eval' || policy.allowEval
|
||||
}
|
||||
|
||||
export function applyToolPolicy(
|
||||
descriptors: readonly ToolDescriptor[],
|
||||
policy: ToolPolicy
|
||||
): ToolDescriptor[] {
|
||||
return descriptors.map((descriptor) => ({
|
||||
...descriptor,
|
||||
enabled: isToolEnabled(descriptor, policy)
|
||||
}))
|
||||
}
|
||||
|
||||
export function readToolPolicyFromEnv(env: NodeJS.ProcessEnv = process.env): ToolPolicy {
|
||||
return {
|
||||
allowEval: env.OPENPENCIL_MCP_EVAL === '1',
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { MAX_RESULT_BYTES, fail, ok, resultTooLargeMessage } from '#mcp/result'
|
|||
import { createToolDescriptors } from '#mcp/tool/manifest'
|
||||
import type { ToolDescriptor, ToolEffect, ToolPolicy } from '#mcp/tool/metadata'
|
||||
import { resolveSafePath, writeToolOutput } from '#mcp/tool/output'
|
||||
import { isToolEnabled } from '#mcp/tool/policy'
|
||||
import { paramToZod } from '#mcp/tool/schema'
|
||||
|
||||
export type RPCSender = (body: Record<string, unknown>) => Promise<unknown>
|
||||
|
|
@ -54,7 +55,6 @@ function descriptorByName(descriptors: readonly ToolDescriptor[]): Map<string, T
|
|||
|
||||
export function registerTools(mcpServer: McpServer, options: RegisterToolsOptions): void {
|
||||
const { policy, sendRPC } = options
|
||||
const disabledTools = new Set(policy.disabledTools)
|
||||
const resolvedRoot = options.mcpRoot ? resolve(options.mcpRoot) : null
|
||||
const descriptors = descriptorByName(createToolDescriptors(resolvedRoot !== null))
|
||||
const register = <InputArgs extends z.ZodObject>(
|
||||
|
|
@ -64,8 +64,7 @@ export function registerTools(mcpServer: McpServer, options: RegisterToolsOption
|
|||
) => {
|
||||
const descriptor = descriptors.get(name)
|
||||
if (!descriptor) throw new Error(`Missing MCP tool descriptor for "${name}"`)
|
||||
if (disabledTools.has(name)) return
|
||||
if (descriptor.availability === 'eval' && !policy.allowEval) return
|
||||
if (!isToolEnabled(descriptor, policy)) return
|
||||
mcpServer.registerTool(
|
||||
name,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -323,8 +323,8 @@
|
|||
"mcpUseDefaultRoot": "Use default",
|
||||
"mcpRootDirectoryDescription": "File tools are limited to this folder. Restart the MCP server to apply changes.",
|
||||
"mcpTools": "Available tools",
|
||||
"mcpReadOnlyTools": "Read-only tools",
|
||||
"mcpDocumentWritingTools": "Document-writing tools",
|
||||
"mcpReadOnlyTools": "Nur-Lese-Werkzeuge",
|
||||
"mcpSideEffectTools": "Werkzeuge mit Nebenwirkungen",
|
||||
"mcpToolsEnabled": "{enabled} of {total} enabled",
|
||||
"mcpEnableAllTools": "Enable all",
|
||||
"mcpSearchTools": "Search MCP tools",
|
||||
|
|
|
|||
|
|
@ -323,8 +323,8 @@
|
|||
"mcpUseDefaultRoot": "Use default",
|
||||
"mcpRootDirectoryDescription": "File tools are limited to this folder. Restart the MCP server to apply changes.",
|
||||
"mcpTools": "Available tools",
|
||||
"mcpReadOnlyTools": "Read-only tools",
|
||||
"mcpDocumentWritingTools": "Document-writing tools",
|
||||
"mcpReadOnlyTools": "Herramientas de solo lectura",
|
||||
"mcpSideEffectTools": "Herramientas con efectos secundarios",
|
||||
"mcpToolsEnabled": "{enabled} of {total} enabled",
|
||||
"mcpEnableAllTools": "Enable all",
|
||||
"mcpSearchTools": "Search MCP tools",
|
||||
|
|
|
|||
|
|
@ -323,8 +323,8 @@
|
|||
"mcpUseDefaultRoot": "Use default",
|
||||
"mcpRootDirectoryDescription": "File tools are limited to this folder. Restart the MCP server to apply changes.",
|
||||
"mcpTools": "Available tools",
|
||||
"mcpReadOnlyTools": "Read-only tools",
|
||||
"mcpDocumentWritingTools": "Document-writing tools",
|
||||
"mcpReadOnlyTools": "Outils en lecture seule",
|
||||
"mcpSideEffectTools": "Outils avec effets secondaires",
|
||||
"mcpToolsEnabled": "{enabled} of {total} enabled",
|
||||
"mcpEnableAllTools": "Enable all",
|
||||
"mcpSearchTools": "Search MCP tools",
|
||||
|
|
|
|||
|
|
@ -323,8 +323,8 @@
|
|||
"mcpUseDefaultRoot": "Use default",
|
||||
"mcpRootDirectoryDescription": "File tools are limited to this folder. Restart the MCP server to apply changes.",
|
||||
"mcpTools": "Available tools",
|
||||
"mcpReadOnlyTools": "Read-only tools",
|
||||
"mcpDocumentWritingTools": "Document-writing tools",
|
||||
"mcpReadOnlyTools": "Strumenti di sola lettura",
|
||||
"mcpSideEffectTools": "Strumenti con effetti collaterali",
|
||||
"mcpToolsEnabled": "{enabled} of {total} enabled",
|
||||
"mcpEnableAllTools": "Enable all",
|
||||
"mcpSearchTools": "Search MCP tools",
|
||||
|
|
|
|||
|
|
@ -323,8 +323,8 @@
|
|||
"mcpUseDefaultRoot": "Use default",
|
||||
"mcpRootDirectoryDescription": "File tools are limited to this folder. Restart the MCP server to apply changes.",
|
||||
"mcpTools": "Available tools",
|
||||
"mcpReadOnlyTools": "Read-only tools",
|
||||
"mcpDocumentWritingTools": "Document-writing tools",
|
||||
"mcpReadOnlyTools": "読み取り専用ツール",
|
||||
"mcpSideEffectTools": "副作用のあるツール",
|
||||
"mcpToolsEnabled": "{enabled} of {total} enabled",
|
||||
"mcpEnableAllTools": "Enable all",
|
||||
"mcpSearchTools": "Search MCP tools",
|
||||
|
|
|
|||
|
|
@ -323,8 +323,8 @@
|
|||
"mcpUseDefaultRoot": "Use default",
|
||||
"mcpRootDirectoryDescription": "File tools are limited to this folder. Restart the MCP server to apply changes.",
|
||||
"mcpTools": "Available tools",
|
||||
"mcpReadOnlyTools": "Read-only tools",
|
||||
"mcpDocumentWritingTools": "Document-writing tools",
|
||||
"mcpReadOnlyTools": "Narzędzia tylko do odczytu",
|
||||
"mcpSideEffectTools": "Narzędzia z efektami ubocznymi",
|
||||
"mcpToolsEnabled": "{enabled} of {total} enabled",
|
||||
"mcpEnableAllTools": "Enable all",
|
||||
"mcpSearchTools": "Search MCP tools",
|
||||
|
|
|
|||
|
|
@ -323,8 +323,8 @@
|
|||
"mcpUseDefaultRoot": "Use default",
|
||||
"mcpRootDirectoryDescription": "File tools are limited to this folder. Restart the MCP server to apply changes.",
|
||||
"mcpTools": "Available tools",
|
||||
"mcpReadOnlyTools": "Read-only tools",
|
||||
"mcpDocumentWritingTools": "Document-writing tools",
|
||||
"mcpReadOnlyTools": "Инструменты только для чтения",
|
||||
"mcpSideEffectTools": "Инструменты с побочными эффектами",
|
||||
"mcpToolsEnabled": "{enabled} of {total} enabled",
|
||||
"mcpEnableAllTools": "Enable all",
|
||||
"mcpSearchTools": "Search MCP tools",
|
||||
|
|
|
|||
|
|
@ -323,8 +323,8 @@
|
|||
"mcpUseDefaultRoot": "Use default",
|
||||
"mcpRootDirectoryDescription": "File tools are limited to this folder. Restart the MCP server to apply changes.",
|
||||
"mcpTools": "Available tools",
|
||||
"mcpReadOnlyTools": "Read-only tools",
|
||||
"mcpDocumentWritingTools": "Document-writing tools",
|
||||
"mcpReadOnlyTools": "只读工具",
|
||||
"mcpSideEffectTools": "有副作用的工具",
|
||||
"mcpToolsEnabled": "{enabled} of {total} enabled",
|
||||
"mcpEnableAllTools": "Enable all",
|
||||
"mcpSearchTools": "Search MCP tools",
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ export const dialogMessageDefaults = {
|
|||
mcpEnableAllTools: 'Enable all',
|
||||
mcpSearchTools: 'Search MCP tools',
|
||||
mcpReadOnlyTools: 'Read-only tools',
|
||||
mcpDocumentWritingTools: 'Document-writing tools',
|
||||
mcpSideEffectTools: 'Tools with side effects',
|
||||
mcpToolsRestartNotice:
|
||||
'Restart the MCP server, then reconnect stdio clients, to apply tool availability changes.',
|
||||
mcpExternalRestartNotice:
|
||||
|
|
|
|||
|
|
@ -42,16 +42,51 @@ export function createAutomationEnvironment(
|
|||
const MAX_CONFIGURATION_BYTES = 70_000
|
||||
const CHILD_EXIT_TIMEOUT_MS = 2_000
|
||||
|
||||
type DevMCPConfigurationErrorStatus = 400 | 413
|
||||
|
||||
class DevMCPConfigurationRequestError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly statusCode: DevMCPConfigurationErrorStatus,
|
||||
options?: ErrorOptions
|
||||
) {
|
||||
super(message, options)
|
||||
this.name = 'DevMCPConfigurationRequestError'
|
||||
}
|
||||
}
|
||||
|
||||
export class DevMCPConfigurationTooLargeError extends DevMCPConfigurationRequestError {
|
||||
constructor() {
|
||||
super('Request body is too large', 413)
|
||||
this.name = 'DevMCPConfigurationTooLargeError'
|
||||
}
|
||||
}
|
||||
|
||||
export class DevMCPConfigurationSyntaxError extends DevMCPConfigurationRequestError {
|
||||
constructor(cause: unknown) {
|
||||
super('Malformed JSON configuration', 400, { cause })
|
||||
this.name = 'DevMCPConfigurationSyntaxError'
|
||||
}
|
||||
}
|
||||
|
||||
export function devMCPConfigurationErrorStatus(error: unknown): 400 | 413 | 500 {
|
||||
return error instanceof DevMCPConfigurationRequestError ? error.statusCode : 500
|
||||
}
|
||||
|
||||
export async function readDevMCPConfiguration(request: IncomingMessage): Promise<unknown> {
|
||||
const chunks: Buffer[] = []
|
||||
let byteLength = 0
|
||||
for await (const chunk of request) {
|
||||
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
||||
byteLength += buffer.byteLength
|
||||
if (byteLength > MAX_CONFIGURATION_BYTES) throw new Error('Request body is too large')
|
||||
if (byteLength > MAX_CONFIGURATION_BYTES) throw new DevMCPConfigurationTooLargeError()
|
||||
chunks.push(buffer)
|
||||
}
|
||||
return JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
||||
try {
|
||||
return JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
||||
} catch (error) {
|
||||
throw new DevMCPConfigurationSyntaxError(error)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: production — bundle MCP server as Tauri sidecar or spawn via shell plugin
|
||||
|
|
@ -159,7 +194,7 @@ export function automationPlugin(authToken: string | null, corsOrigin: string):
|
|||
response.statusCode = 204
|
||||
response.end()
|
||||
} catch (error) {
|
||||
response.statusCode = 500
|
||||
response.statusCode = devMCPConfigurationErrorStatus(error)
|
||||
response.end(error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
})()
|
||||
|
|
|
|||
|
|
@ -152,7 +152,8 @@ export function createMCPRuntimeService(dependencies: MCPRuntimeDependencies) {
|
|||
stop: () => enqueue(() => stopOperation(true)),
|
||||
restart: () =>
|
||||
enqueue(async () => {
|
||||
await stopOperation(false)
|
||||
const stopResult = await stopOperation(false)
|
||||
if (!stopResult.ok) return stopResult
|
||||
if (!activeStore) {
|
||||
const error = new Error('Editor is not ready')
|
||||
state.status = 'error'
|
||||
|
|
|
|||
|
|
@ -201,11 +201,11 @@ function enableAllTools(): void {
|
|||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2 rounded bg-input px-2.5 py-2">
|
||||
<span class="text-[10px] text-surface">{{ dialogs.mcpDocumentWritingTools }}</span>
|
||||
<span class="text-[10px] text-surface">{{ dialogs.mcpSideEffectTools }}</span>
|
||||
<AppSwitch
|
||||
:model-value="modificationToolsStatus.enabled"
|
||||
:state="modificationToolsStatus.state"
|
||||
:label="dialogs.mcpDocumentWritingTools"
|
||||
:label="dialogs.mcpSideEffectTools"
|
||||
data-test-id="settings-mcp-modification-tools"
|
||||
@update:model-value="setMCPToolCategoryEnabled('write', $event)"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ function descriptor(name = 'get_page_tree'): ToolDescriptor {
|
|||
description: name,
|
||||
effect: 'read',
|
||||
availability: 'default',
|
||||
capabilities: ['document:read']
|
||||
capabilities: ['document:read'],
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -104,6 +105,29 @@ describe('MCP runtime service', () => {
|
|||
expect(catalogs.at(-1)).toEqual([])
|
||||
})
|
||||
|
||||
test('does not start a replacement when shutdown fails', async () => {
|
||||
let spawnCalls = 0
|
||||
const { service } = setup({
|
||||
spawn: async () => {
|
||||
spawnCalls++
|
||||
return {
|
||||
authToken: 'token',
|
||||
managed: true,
|
||||
disconnect: () => {
|
||||
throw new Error('shutdown failed')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
await service.start(getStore)
|
||||
|
||||
const result = await service.restart()
|
||||
|
||||
expect(result.ok).toBe(false)
|
||||
expect(spawnCalls).toBe(1)
|
||||
expect(service.state.status).toBe('error')
|
||||
})
|
||||
|
||||
test('restarts with the retained editor store inside one lifecycle operation', async () => {
|
||||
const { calls, service } = setup()
|
||||
await service.start(getStore)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ import { describe, expect, test } from 'bun:test'
|
|||
import { Readable } from 'node:stream'
|
||||
|
||||
import {
|
||||
DevMCPConfigurationSyntaxError,
|
||||
DevMCPConfigurationTooLargeError,
|
||||
createAutomationEnvironment,
|
||||
devMCPConfigurationErrorStatus,
|
||||
readDevMCPConfiguration
|
||||
} from '@/app/automation/bridge/vite-plugin'
|
||||
|
||||
|
|
@ -43,10 +46,20 @@ describe('MCP Vite development server', () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('rejects configuration bodies above the byte limit', async () => {
|
||||
const request = Readable.from([Buffer.alloc(70_001)])
|
||||
await expect(readDevMCPConfiguration(request as never)).rejects.toThrow(
|
||||
'Request body is too large'
|
||||
test('classifies malformed and oversized configuration requests', async () => {
|
||||
const malformed = Readable.from(['{'])
|
||||
const malformedError = await readDevMCPConfiguration(malformed as never).catch(
|
||||
(error: unknown) => error
|
||||
)
|
||||
expect(malformedError).toBeInstanceOf(DevMCPConfigurationSyntaxError)
|
||||
expect(devMCPConfigurationErrorStatus(malformedError)).toBe(400)
|
||||
|
||||
const oversized = Readable.from([Buffer.alloc(70_001)])
|
||||
const oversizedError = await readDevMCPConfiguration(oversized as never).catch(
|
||||
(error: unknown) => error
|
||||
)
|
||||
expect(oversizedError).toBeInstanceOf(DevMCPConfigurationTooLargeError)
|
||||
expect(devMCPConfigurationErrorStatus(oversizedError)).toBe(413)
|
||||
expect(devMCPConfigurationErrorStatus(new Error('restart failed'))).toBe(500)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ describe('MCP server', () => {
|
|||
expect(byName.get('switch_page')?.effect).toBe('read')
|
||||
expect(byName.get('viewport_set')?.effect).toBe('read')
|
||||
expect(byName.get('export_image')?.effect).toBe('read')
|
||||
expect(byName.get('save_file')?.effect).toBe('write')
|
||||
expect(byName.get('update_node')?.effect).toBe('write')
|
||||
expect(byName.get('new_document')?.capabilities).toEqual(['document:write', 'filesystem:write'])
|
||||
expect(byName.get('eval')?.availability).toBe('eval')
|
||||
|
|
@ -176,9 +177,10 @@ describe('MCP server', () => {
|
|||
headers: { Authorization: `Bearer ${TEST_CLIENT_AUTH_TOKEN}` }
|
||||
})
|
||||
const health = (await healthResponse.json()) as HealthResponse
|
||||
const catalogNames = (health.tools ?? []).map((tool) => tool.name)
|
||||
expect(catalogNames).toContain('create_shape')
|
||||
expect(catalogNames).toContain('list_documents')
|
||||
const descriptors = health.tools ?? []
|
||||
expect(descriptors.find((tool) => tool.name === 'create_shape')?.enabled).toBe(false)
|
||||
expect(descriptors.find((tool) => tool.name === 'list_documents')?.enabled).toBe(false)
|
||||
expect(descriptors.find((tool) => tool.name === 'get_page_tree')?.enabled).toBe(true)
|
||||
})
|
||||
|
||||
test('tools expose standard MCP effect annotations', async () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue