diff --git a/src/tools/localServer.test.mjs b/src/tools/localServer.test.mjs index 19e09516b..3cb61ebbb 100644 --- a/src/tools/localServer.test.mjs +++ b/src/tools/localServer.test.mjs @@ -4,7 +4,7 @@ import { PassThrough } from 'node:stream'; import { createLocalMcpServer } from '../../server/mcp/server.js'; import { createApiFetch } from '../../server/mcp/services.js'; import { parseArgs, serveStdio } from '../../server/mcp/stdio.js'; -import { coreTools } from './index.js'; +import { coreTools, toolsForSurface } from './index.js'; const usgs = { type: 'FeatureCollection', @@ -70,7 +70,7 @@ test('the stdio server answers newline-delimited requests using only its data so // Every Core tool's services are composed locally. assert.deepEqual( byId.get(2).result.tools.map((tool) => tool.name), - coreTools.map((tool) => tool.name), + toolsForSurface(coreTools, 'mcp').map((tool) => tool.name), ); assert.equal( byId.get(3).result.content[0].text, diff --git a/src/tools/surfaces.js b/src/tools/surfaces.js index 73a45e499..7b1030f88 100644 --- a/src/tools/surfaces.js +++ b/src/tools/surfaces.js @@ -2,6 +2,8 @@ * Which surfaces offer each tool. A tool is on every surface unless this * table turns it off; edit an entry to turn a tool on or off for MCP, voice, * or both. Applications can pass overrides when they compose a surface. + * Tools off a surface stay reachable from tools that combine others, such as + * situation_brief using get_weather. */ import { ToolError } from './catalog.js'; @@ -9,6 +11,7 @@ import { ToolError } from './catalog.js'; export const SURFACES = Object.freeze(['mcp', 'voice']); const voiceOff = Object.freeze({ voice: false }); +const mcpOff = Object.freeze({ mcp: false }); export const TOOL_SURFACES = Object.freeze({ // Voice runs inside the app, so a link to it adds nothing. @@ -19,8 +22,8 @@ export const TOOL_SURFACES = Object.freeze({ get_weather_map: voiceOff, get_recent_imagery: voiceOff, get_cctv_snapshot: voiceOff, - // Voice shows the HUD caption in the app itself. - get_hud_caption: voiceOff, + // The app shows its HUD caption itself, and assistants write their own. + get_hud_caption: Object.freeze({ voice: false, mcp: false }), // Voice answers these from the loaded layers with analyst_query. aircraft_in_area: voiceOff, vessels_in_area: voiceOff, @@ -30,6 +33,17 @@ export const TOOL_SURFACES = Object.freeze({ get_active_fires: voiceOff, find_infrastructure: voiceOff, satellites_overhead: voiceOff, + // MCP leads with what the globe shows; assistants already search places, + // plan routes and report weather, and these layers add little in a chat. + search_places: mcpOff, + places_nearby: mcpOff, + plan_route: mcpOff, + get_weather: mcpOff, + get_wind: mcpOff, + get_regional_brief: mcpOff, + find_radio_stations: mcpOff, + get_bike_share: mcpOff, + get_transit_vehicles: mcpOff, }); /** diff --git a/src/tools/surfaces.test.mjs b/src/tools/surfaces.test.mjs index 5fc7424da..255b7c9fd 100644 --- a/src/tools/surfaces.test.mjs +++ b/src/tools/surfaces.test.mjs @@ -11,17 +11,32 @@ import { const names = (tools) => tools.map((tool) => tool.name); -test('MCP offers every tool and voice leaves out the table entries', () => { - assert.deepEqual(names(toolsForSurface(coreTools, 'mcp')), names(coreTools)); +test('each surface offers the tools the table does not turn off', () => { + for (const surface of ['mcp', 'voice']) { + const offered = names(toolsForSurface(coreTools, surface)); + const expected = names(coreTools).filter( + (name) => TOOL_SURFACES[name]?.[surface] !== false, + ); + assert.deepEqual(offered, expected, surface); + } + const mcp = names(toolsForSurface(coreTools, 'mcp')); + for (const name of [ + 'open_in_gods_eye_view', + 'situation_brief', + 'aircraft_in_area', + ]) + assert.ok(mcp.includes(name), name); + for (const name of [ + 'search_places', + 'plan_route', + 'get_weather', + 'get_hud_caption', + ]) + assert.ok(!mcp.includes(name), name); const voice = names(toolsForSurface(coreTools, 'voice')); - assert.equal( - voice.length, - coreTools.length - Object.keys(TOOL_SURFACES).length, - ); - for (const name of Object.keys(TOOL_SURFACES)) - assert.ok(!voice.includes(name), name); assert.ok(voice.includes('get_weather')); assert.ok(voice.includes('military_awareness')); + assert.ok(!voice.includes('aircraft_in_area')); }); test('overrides turn a tool on or off for one surface', () => { @@ -44,7 +59,8 @@ test('every table entry names a core tool and a known surface', () => { const names = new Set(coreTools.map((tool) => tool.name)); for (const [name, entry] of Object.entries(TOOL_SURFACES)) { assert.ok(names.has(name), name); - assert.deepEqual(Object.keys(entry), ['voice']); + for (const key of Object.keys(entry)) + assert.ok(['voice', 'mcp'].includes(key), `${name}: ${key}`); } });