fix: mark MCP tools approval-safe
This commit is contained in:
@@ -26,6 +26,12 @@ import path from 'node:path'
|
||||
|
||||
const WS_UI_PORT = parseInt(process.env.WS_UI_PORT || '9711', 10)
|
||||
const WS_UI_URL = `ws://localhost:${WS_UI_PORT}`
|
||||
const LOCAL_READ_ONLY_TOOL_ANNOTATIONS = Object.freeze({
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: false,
|
||||
})
|
||||
|
||||
// Connect as a WebSocket CLIENT to the UI bridge (run by ws-bridge.js).
|
||||
// The bridge relays messages to all other clients (the React frontend).
|
||||
@@ -107,6 +113,7 @@ const TOOLS = [
|
||||
{
|
||||
name: 'search_notes',
|
||||
description: 'Full-text search across vault notes by title or content. Returns matching paths, titles, and snippets.',
|
||||
annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -119,6 +126,7 @@ const TOOLS = [
|
||||
{
|
||||
name: 'get_vault_context',
|
||||
description: 'Get vault orientation for the active Tolaria vaults: entity types, AGENTS.md instructions, note count, folders, and recent notes.',
|
||||
annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -129,6 +137,7 @@ const TOOLS = [
|
||||
{
|
||||
name: 'list_vaults',
|
||||
description: 'List the current active Tolaria vaults available to MCP tools, including whether each vault has AGENTS.md instructions.',
|
||||
annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
@@ -137,6 +146,7 @@ const TOOLS = [
|
||||
{
|
||||
name: 'get_note',
|
||||
description: 'Read a note with parsed YAML frontmatter and markdown content. Returns {path, frontmatter, content}.',
|
||||
annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -149,6 +159,7 @@ const TOOLS = [
|
||||
{
|
||||
name: 'open_note',
|
||||
description: 'Open a note in the Tolaria UI as a new tab. Use after creating or editing a note so the user can see it.',
|
||||
annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -161,6 +172,7 @@ const TOOLS = [
|
||||
{
|
||||
name: 'highlight_editor',
|
||||
description: 'Visually highlight a UI element in Tolaria (editor, tab, properties panel, or note list). The highlight auto-clears after a short delay.',
|
||||
annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -173,6 +185,7 @@ const TOOLS = [
|
||||
{
|
||||
name: 'refresh_vault',
|
||||
description: 'Trigger a vault rescan so new or modified files appear immediately in the Tolaria note list.',
|
||||
annotations: LOCAL_READ_ONLY_TOOL_ANNOTATIONS,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
||||
@@ -9,6 +9,8 @@ import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { clearTimeout, setTimeout } from 'node:timers'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
import {
|
||||
findMarkdownFiles, getNote, searchNotes, vaultContext,
|
||||
} from './vault.js'
|
||||
@@ -305,6 +307,34 @@ describe('requireVaultPath', () => {
|
||||
})
|
||||
|
||||
describe('stdio process lifecycle', () => {
|
||||
it('advertises local vault tools as approval-safe for MCP clients', async () => {
|
||||
const { client, stderr } = await connectMcpClient()
|
||||
|
||||
try {
|
||||
const { tools } = await client.listTools()
|
||||
const toolsByName = new Map(tools.map(tool => [tool.name, tool]))
|
||||
const safeReadTools = [
|
||||
'search_notes',
|
||||
'get_vault_context',
|
||||
'list_vaults',
|
||||
'get_note',
|
||||
'open_note',
|
||||
'highlight_editor',
|
||||
'refresh_vault',
|
||||
]
|
||||
|
||||
for (const name of safeReadTools) {
|
||||
const tool = toolsByName.get(name)
|
||||
assert.ok(tool, `Missing MCP tool: ${name}`)
|
||||
assert.equal(tool.annotations?.readOnlyHint, true, `${name} should not require destructive approval`)
|
||||
assert.equal(tool.annotations?.destructiveHint, false, `${name} should not be treated as destructive`)
|
||||
assert.equal(tool.annotations?.openWorldHint, false, `${name} should stay scoped to local active vaults`)
|
||||
}
|
||||
} finally {
|
||||
await closeMcpClient(client, stderr)
|
||||
}
|
||||
})
|
||||
|
||||
it('exits when the MCP client closes stdin', async () => {
|
||||
const child = spawn(process.execPath, ['index.js'], {
|
||||
cwd: MCP_SERVER_DIR,
|
||||
@@ -332,6 +362,41 @@ describe('stdio process lifecycle', () => {
|
||||
})
|
||||
})
|
||||
|
||||
async function connectMcpClient() {
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: ['index.js'],
|
||||
cwd: MCP_SERVER_DIR,
|
||||
env: { ...process.env, VAULT_PATH: tmpDir, WS_UI_PORT: '65534' },
|
||||
stderr: 'pipe',
|
||||
})
|
||||
const stderr = collectTransportStderr(transport)
|
||||
const client = new Client(
|
||||
{ name: 'tolaria-mcp-test-client', version: '0.0.0' },
|
||||
{ capabilities: {} },
|
||||
)
|
||||
|
||||
await client.connect(transport)
|
||||
return { client, stderr }
|
||||
}
|
||||
|
||||
function collectTransportStderr(transport) {
|
||||
const chunks = []
|
||||
transport.stderr?.setEncoding('utf8')
|
||||
transport.stderr?.on('data', chunk => {
|
||||
chunks.push(chunk)
|
||||
})
|
||||
return () => chunks.join('')
|
||||
}
|
||||
|
||||
async function closeMcpClient(client, stderr) {
|
||||
try {
|
||||
await client.close()
|
||||
} catch (error) {
|
||||
assert.fail(`Failed to close MCP test client: ${error.message}\n${stderr()}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function assertRejectsOutsideVault(prefix, resolveNotePath) {
|
||||
const outsideDir = await mkdtemp(path.join(os.tmpdir(), prefix))
|
||||
const outsideNote = path.join(outsideDir, 'outside.md')
|
||||
|
||||
Reference in New Issue
Block a user