From af8856cedfc55127b2e3d9710af2aa17bb12afa8 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Thu, 1 Oct 2026 21:25:27 -0700 Subject: [PATCH] feat: lead the MCP tool list with what the globe shows MCP no longer lists place search, routing, plain weather and wind, the regional brief, the HUD caption, radio, bike share and transit. Assistants already cover these or they add little without the globe; voice keeps them, and tools that combine others still reach them. --- src/tools/localServer.test.mjs | 4 ++-- src/tools/surfaces.js | 18 ++++++++++++++++-- src/tools/surfaces.test.mjs | 34 +++++++++++++++++++++++++--------- 3 files changed, 43 insertions(+), 13 deletions(-) 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}`); } });